callman-core 1.36.0 → 1.38.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 (204) hide show
  1. package/dist/assertions/dbAssertions.d.ts +7 -0
  2. package/dist/assertions/dbAssertions.d.ts.map +1 -0
  3. package/dist/assertions/dbAssertions.js +191 -0
  4. package/dist/assertions/dbAssertions.js.map +1 -0
  5. package/dist/assertions/index.d.ts +4 -0
  6. package/dist/assertions/index.d.ts.map +1 -0
  7. package/dist/assertions/index.js +3 -0
  8. package/dist/assertions/index.js.map +1 -0
  9. package/dist/assertions/redisAssertions.d.ts +9 -0
  10. package/dist/assertions/redisAssertions.d.ts.map +1 -0
  11. package/dist/assertions/redisAssertions.js +159 -0
  12. package/dist/assertions/redisAssertions.js.map +1 -0
  13. package/dist/assertions/shared.d.ts +5 -0
  14. package/dist/assertions/shared.d.ts.map +1 -0
  15. package/dist/assertions/shared.js +25 -0
  16. package/dist/assertions/shared.js.map +1 -0
  17. package/dist/assertions/types.d.ts +46 -0
  18. package/dist/assertions/types.d.ts.map +1 -0
  19. package/dist/assertions/types.js +5 -0
  20. package/dist/assertions/types.js.map +1 -0
  21. package/dist/host/connections.d.ts +11 -0
  22. package/dist/host/connections.d.ts.map +1 -1
  23. package/dist/host/connections.js +22 -0
  24. package/dist/host/connections.js.map +1 -1
  25. package/dist/index.d.ts +2 -0
  26. package/dist/index.d.ts.map +1 -1
  27. package/dist/index.js +2 -0
  28. package/dist/index.js.map +1 -1
  29. package/dist/loadtest/compare.d.ts +32 -0
  30. package/dist/loadtest/compare.d.ts.map +1 -0
  31. package/dist/loadtest/compare.js +55 -0
  32. package/dist/loadtest/compare.js.map +1 -0
  33. package/dist/loadtest/flow-eval.d.ts +21 -0
  34. package/dist/loadtest/flow-eval.d.ts.map +1 -0
  35. package/dist/loadtest/flow-eval.js +199 -0
  36. package/dist/loadtest/flow-eval.js.map +1 -0
  37. package/dist/loadtest/from-collection.d.ts +51 -0
  38. package/dist/loadtest/from-collection.d.ts.map +1 -0
  39. package/dist/loadtest/from-collection.js +131 -0
  40. package/dist/loadtest/from-collection.js.map +1 -0
  41. package/dist/loadtest/histogram.d.ts +41 -0
  42. package/dist/loadtest/histogram.d.ts.map +1 -0
  43. package/dist/loadtest/histogram.js +166 -0
  44. package/dist/loadtest/histogram.js.map +1 -0
  45. package/dist/loadtest/index.d.ts +18 -0
  46. package/dist/loadtest/index.d.ts.map +1 -0
  47. package/dist/loadtest/index.js +14 -0
  48. package/dist/loadtest/index.js.map +1 -0
  49. package/dist/loadtest/node/index.d.ts +4 -0
  50. package/dist/loadtest/node/index.d.ts.map +1 -0
  51. package/dist/loadtest/node/index.js +4 -0
  52. package/dist/loadtest/node/index.js.map +1 -0
  53. package/dist/loadtest/node/metrics.d.ts +84 -0
  54. package/dist/loadtest/node/metrics.d.ts.map +1 -0
  55. package/dist/loadtest/node/metrics.js +396 -0
  56. package/dist/loadtest/node/metrics.js.map +1 -0
  57. package/dist/loadtest/node/orchestrator.d.ts +24 -0
  58. package/dist/loadtest/node/orchestrator.d.ts.map +1 -0
  59. package/dist/loadtest/node/orchestrator.js +363 -0
  60. package/dist/loadtest/node/orchestrator.js.map +1 -0
  61. package/dist/loadtest/node/protocol.d.ts +102 -0
  62. package/dist/loadtest/node/protocol.d.ts.map +1 -0
  63. package/dist/loadtest/node/protocol.js +17 -0
  64. package/dist/loadtest/node/protocol.js.map +1 -0
  65. package/dist/loadtest/node/setup.d.ts +6 -0
  66. package/dist/loadtest/node/setup.d.ts.map +1 -0
  67. package/dist/loadtest/node/setup.js +100 -0
  68. package/dist/loadtest/node/setup.js.map +1 -0
  69. package/dist/loadtest/node/step-compiler.d.ts +29 -0
  70. package/dist/loadtest/node/step-compiler.d.ts.map +1 -0
  71. package/dist/loadtest/node/step-compiler.js +229 -0
  72. package/dist/loadtest/node/step-compiler.js.map +1 -0
  73. package/dist/loadtest/node/transport.d.ts +66 -0
  74. package/dist/loadtest/node/transport.d.ts.map +1 -0
  75. package/dist/loadtest/node/transport.js +414 -0
  76. package/dist/loadtest/node/transport.js.map +1 -0
  77. package/dist/loadtest/node/worker-core.d.ts +77 -0
  78. package/dist/loadtest/node/worker-core.d.ts.map +1 -0
  79. package/dist/loadtest/node/worker-core.js +744 -0
  80. package/dist/loadtest/node/worker-core.js.map +1 -0
  81. package/dist/loadtest/node/worker.d.ts +2 -0
  82. package/dist/loadtest/node/worker.d.ts.map +1 -0
  83. package/dist/loadtest/node/worker.js +28 -0
  84. package/dist/loadtest/node/worker.js.map +1 -0
  85. package/dist/loadtest/plan-file.d.ts +91 -0
  86. package/dist/loadtest/plan-file.d.ts.map +1 -0
  87. package/dist/loadtest/plan-file.js +134 -0
  88. package/dist/loadtest/plan-file.js.map +1 -0
  89. package/dist/loadtest/plan.d.ts +47 -0
  90. package/dist/loadtest/plan.d.ts.map +1 -0
  91. package/dist/loadtest/plan.js +259 -0
  92. package/dist/loadtest/plan.js.map +1 -0
  93. package/dist/loadtest/report.d.ts +8 -0
  94. package/dist/loadtest/report.d.ts.map +1 -0
  95. package/dist/loadtest/report.js +211 -0
  96. package/dist/loadtest/report.js.map +1 -0
  97. package/dist/loadtest/thresholds.d.ts +55 -0
  98. package/dist/loadtest/thresholds.d.ts.map +1 -0
  99. package/dist/loadtest/thresholds.js +203 -0
  100. package/dist/loadtest/thresholds.js.map +1 -0
  101. package/dist/loadtest/types.d.ts +500 -0
  102. package/dist/loadtest/types.d.ts.map +1 -0
  103. package/dist/loadtest/types.js +8 -0
  104. package/dist/loadtest/types.js.map +1 -0
  105. package/dist/loadtest/validate.d.ts +30 -0
  106. package/dist/loadtest/validate.d.ts.map +1 -0
  107. package/dist/loadtest/validate.js +162 -0
  108. package/dist/loadtest/validate.js.map +1 -0
  109. package/dist/runner/requestAssertions.d.ts +19 -0
  110. package/dist/runner/requestAssertions.d.ts.map +1 -0
  111. package/dist/runner/requestAssertions.js +45 -0
  112. package/dist/runner/requestAssertions.js.map +1 -0
  113. package/dist/runner/requestRunner.d.ts.map +1 -1
  114. package/dist/runner/requestRunner.js +25 -0
  115. package/dist/runner/requestRunner.js.map +1 -1
  116. package/dist/runner/runCollection.d.ts.map +1 -1
  117. package/dist/runner/runCollection.js +4 -1
  118. package/dist/runner/runCollection.js.map +1 -1
  119. package/dist/runner/runRequestWithDataset.d.ts.map +1 -1
  120. package/dist/runner/runRequestWithDataset.js +15 -2
  121. package/dist/runner/runRequestWithDataset.js.map +1 -1
  122. package/dist/templating/compile.d.ts +8 -0
  123. package/dist/templating/compile.d.ts.map +1 -0
  124. package/dist/templating/compile.js +110 -0
  125. package/dist/templating/compile.js.map +1 -0
  126. package/dist/templating/index.d.ts +2 -0
  127. package/dist/templating/index.d.ts.map +1 -1
  128. package/dist/templating/index.js +1 -0
  129. package/dist/templating/index.js.map +1 -1
  130. package/dist/templating/template-engine.d.ts +6 -0
  131. package/dist/templating/template-engine.d.ts.map +1 -1
  132. package/dist/templating/template-engine.js +6 -6
  133. package/dist/templating/template-engine.js.map +1 -1
  134. package/dist/types/index.d.ts +10 -0
  135. package/dist/types/index.d.ts.map +1 -1
  136. package/dist-cjs/assertions/dbAssertions.js +195 -0
  137. package/dist-cjs/assertions/dbAssertions.js.map +1 -0
  138. package/dist-cjs/assertions/index.js +8 -0
  139. package/dist-cjs/assertions/index.js.map +1 -0
  140. package/dist-cjs/assertions/redisAssertions.js +163 -0
  141. package/dist-cjs/assertions/redisAssertions.js.map +1 -0
  142. package/dist-cjs/assertions/shared.js +30 -0
  143. package/dist-cjs/assertions/shared.js.map +1 -0
  144. package/dist-cjs/assertions/types.js +6 -0
  145. package/dist-cjs/assertions/types.js.map +1 -0
  146. package/dist-cjs/host/connections.js +24 -1
  147. package/dist-cjs/host/connections.js.map +1 -1
  148. package/dist-cjs/index.js +2 -0
  149. package/dist-cjs/index.js.map +1 -1
  150. package/dist-cjs/loadtest/compare.js +59 -0
  151. package/dist-cjs/loadtest/compare.js.map +1 -0
  152. package/dist-cjs/loadtest/flow-eval.js +205 -0
  153. package/dist-cjs/loadtest/flow-eval.js.map +1 -0
  154. package/dist-cjs/loadtest/from-collection.js +139 -0
  155. package/dist-cjs/loadtest/from-collection.js.map +1 -0
  156. package/dist-cjs/loadtest/histogram.js +171 -0
  157. package/dist-cjs/loadtest/histogram.js.map +1 -0
  158. package/dist-cjs/loadtest/index.js +78 -0
  159. package/dist-cjs/loadtest/index.js.map +1 -0
  160. package/dist-cjs/loadtest/node/index.js +12 -0
  161. package/dist-cjs/loadtest/node/index.js.map +1 -0
  162. package/dist-cjs/loadtest/node/metrics.js +402 -0
  163. package/dist-cjs/loadtest/node/metrics.js.map +1 -0
  164. package/dist-cjs/loadtest/node/orchestrator.js +372 -0
  165. package/dist-cjs/loadtest/node/orchestrator.js.map +1 -0
  166. package/dist-cjs/loadtest/node/protocol.js +20 -0
  167. package/dist-cjs/loadtest/node/protocol.js.map +1 -0
  168. package/dist-cjs/loadtest/node/setup.js +105 -0
  169. package/dist-cjs/loadtest/node/setup.js.map +1 -0
  170. package/dist-cjs/loadtest/node/step-compiler.js +234 -0
  171. package/dist-cjs/loadtest/node/step-compiler.js.map +1 -0
  172. package/dist-cjs/loadtest/node/transport.js +422 -0
  173. package/dist-cjs/loadtest/node/transport.js.map +1 -0
  174. package/dist-cjs/loadtest/node/worker-core.js +749 -0
  175. package/dist-cjs/loadtest/node/worker-core.js.map +1 -0
  176. package/dist-cjs/loadtest/node/worker.js +30 -0
  177. package/dist-cjs/loadtest/node/worker.js.map +1 -0
  178. package/dist-cjs/loadtest/plan-file.js +143 -0
  179. package/dist-cjs/loadtest/plan-file.js.map +1 -0
  180. package/dist-cjs/loadtest/plan.js +271 -0
  181. package/dist-cjs/loadtest/plan.js.map +1 -0
  182. package/dist-cjs/loadtest/report.js +220 -0
  183. package/dist-cjs/loadtest/report.js.map +1 -0
  184. package/dist-cjs/loadtest/thresholds.js +213 -0
  185. package/dist-cjs/loadtest/thresholds.js.map +1 -0
  186. package/dist-cjs/loadtest/types.js +9 -0
  187. package/dist-cjs/loadtest/types.js.map +1 -0
  188. package/dist-cjs/loadtest/validate.js +169 -0
  189. package/dist-cjs/loadtest/validate.js.map +1 -0
  190. package/dist-cjs/runner/requestAssertions.js +49 -0
  191. package/dist-cjs/runner/requestAssertions.js.map +1 -0
  192. package/dist-cjs/runner/requestRunner.js +25 -0
  193. package/dist-cjs/runner/requestRunner.js.map +1 -1
  194. package/dist-cjs/runner/runCollection.js +4 -1
  195. package/dist-cjs/runner/runCollection.js.map +1 -1
  196. package/dist-cjs/runner/runRequestWithDataset.js +15 -2
  197. package/dist-cjs/runner/runRequestWithDataset.js.map +1 -1
  198. package/dist-cjs/templating/compile.js +114 -0
  199. package/dist-cjs/templating/compile.js.map +1 -0
  200. package/dist-cjs/templating/index.js +3 -1
  201. package/dist-cjs/templating/index.js.map +1 -1
  202. package/dist-cjs/templating/template-engine.js +14 -12
  203. package/dist-cjs/templating/template-engine.js.map +1 -1
  204. package/package.json +10 -1
@@ -0,0 +1,749 @@
1
+ "use strict";
2
+ // One load worker: owns a share of the virtual users (closed model) or of the
3
+ // iteration arrivals (open model), drives them over its own HTTP pool, and
4
+ // reports compact per-interval deltas to the orchestrator.
5
+ //
6
+ // The same class runs inside a `worker_threads` Worker (see worker.ts) and
7
+ // inline on the calling thread (small runs, tests, hosts that can't resolve a
8
+ // worker file) — the orchestrator can't tell the difference.
9
+ Object.defineProperty(exports, "__esModule", { value: true });
10
+ exports.LoadWorker = exports.createSharedCounterStore = void 0;
11
+ const node_perf_hooks_1 = require("node:perf_hooks");
12
+ const executeRequest_js_1 = require("../../request/executeRequest.js");
13
+ const compile_js_1 = require("../../templating/compile.js");
14
+ const template_engine_js_1 = require("../../templating/template-engine.js");
15
+ const histogram_js_1 = require("../histogram.js");
16
+ const plan_js_1 = require("../plan.js");
17
+ const flow_eval_js_1 = require("../flow-eval.js");
18
+ const step_compiler_js_1 = require("./step-compiler.js");
19
+ const transport_js_1 = require("./transport.js");
20
+ const FLUSH_INTERVAL_MS = 500;
21
+ const SLOWEST_KEEP = 20;
22
+ const MONITOR_CAP_PER_INTERVAL = 40;
23
+ const MONITOR_BODY_LIMIT = 64 * 1024;
24
+ const CAPTURE_BODY_LIMIT = 1024 * 1024;
25
+ const DEFAULT_OK = { min: 200, max: 399 };
26
+ /**
27
+ * Arrivals are dispatched from a 2 ms timer, so an iteration due a moment ago
28
+ * legitimately starts up to a couple of milliseconds late. That is generator
29
+ * granularity, not a stalled server, so it is not charged to the latency; a
30
+ * real stall (the thing coordinated-omission correction exists for) is far
31
+ * larger than this.
32
+ */
33
+ const SCHEDULER_TOLERANCE_MS = 6;
34
+ const LEGACY_FAKER_ALIASES = {
35
+ $randomUuid: "$randomUUID",
36
+ $randomString: "random.string(12)",
37
+ $randomInt: "random.int(0,1000000)",
38
+ };
39
+ class Interval {
40
+ requests = 0;
41
+ failed = 0;
42
+ networkErrors = 0;
43
+ iterations = 0;
44
+ dropped = 0;
45
+ bytesSent = 0;
46
+ bytesReceived = 0;
47
+ checksPassed = 0;
48
+ checksFailed = 0;
49
+ newConnections = 0;
50
+ blockedRequests = 0;
51
+ statusClass = { c2: 0, c3: 0, c4: 0, c5: 0, c0: 0 };
52
+ statusCodes = {};
53
+ duration = new histogram_js_1.LatencyHistogram();
54
+ corrected = new histogram_js_1.LatencyHistogram();
55
+ waiting = new histogram_js_1.LatencyHistogram();
56
+ receiving = new histogram_js_1.LatencyHistogram();
57
+ sending = new histogram_js_1.LatencyHistogram();
58
+ blocked = new histogram_js_1.LatencyHistogram();
59
+ connecting = new histogram_js_1.LatencyHistogram();
60
+ tls = new histogram_js_1.LatencyHistogram();
61
+ iteration = new histogram_js_1.LatencyHistogram();
62
+ steps = new Map();
63
+ errors = new Map();
64
+ step(id) {
65
+ let entry = this.steps.get(id);
66
+ if (!entry) {
67
+ entry = { requests: 0, failed: 0, checksPassed: 0, checksFailed: 0, checks: {}, statusCodes: {}, duration: new histogram_js_1.LatencyHistogram() };
68
+ this.steps.set(id, entry);
69
+ }
70
+ return entry;
71
+ }
72
+ }
73
+ const sleep = (ms, signal) => new Promise((resolve) => {
74
+ if (ms <= 0 || signal.aborted) {
75
+ resolve();
76
+ return;
77
+ }
78
+ const timer = setTimeout(() => {
79
+ signal.removeEventListener("abort", onAbort);
80
+ resolve();
81
+ }, ms);
82
+ const onAbort = () => {
83
+ clearTimeout(timer);
84
+ resolve();
85
+ };
86
+ signal.addEventListener("abort", onAbort, { once: true });
87
+ });
88
+ const randomBetween = (min, max) => max <= min ? Math.max(0, min) : Math.floor(min + Math.random() * (max - min + 1));
89
+ const yieldToEventLoop = () => new Promise((resolve) => setImmediate(resolve));
90
+ const createSharedCounterStore = (shared, slots) => {
91
+ const cells = new BigInt64Array(shared);
92
+ const local = new Map();
93
+ return {
94
+ getNextValue: (key, startAt = 1) => {
95
+ const slot = slots[key];
96
+ if (slot)
97
+ return Number(Atomics.add(cells, slot.slot, 1n)) + 1 + (slot.start - 1);
98
+ const next = (local.get(key) ?? startAt - 1) + 1;
99
+ local.set(key, next);
100
+ return next;
101
+ },
102
+ snapshot: () => [],
103
+ hydrate: () => undefined,
104
+ reset: () => undefined,
105
+ };
106
+ };
107
+ exports.createSharedCounterStore = createSharedCounterStore;
108
+ class LoadWorker {
109
+ init;
110
+ emit;
111
+ plan;
112
+ transport;
113
+ controller = new AbortController();
114
+ counters;
115
+ counterStore;
116
+ iterationSteps;
117
+ baseVars;
118
+ oauth;
119
+ lag = (0, node_perf_hooks_1.monitorEventLoopDelay)({ resolution: 10 });
120
+ datasetRows;
121
+ fakerBindings = [];
122
+ okRange;
123
+ monitorEnabled;
124
+ monitorEvery;
125
+ cookieJar;
126
+ timeoutMs;
127
+ maxRedirects;
128
+ graceMs;
129
+ correct;
130
+ maxShare;
131
+ startPerf = 0;
132
+ interval = new Interval();
133
+ flushTimer = null;
134
+ scheduleTimer = null;
135
+ ending = false;
136
+ stopRequested = false;
137
+ hardDeadline = Number.POSITIVE_INFINITY;
138
+ finished = false;
139
+ resolveFinished = null;
140
+ vus = [];
141
+ freeVus = [];
142
+ nextVuSeq = 0;
143
+ inflight = 0;
144
+ issued = 0;
145
+ slowest = [];
146
+ monitorBuffer = [];
147
+ oauthCache = new Map();
148
+ constructor(init, emit) {
149
+ this.init = init;
150
+ this.emit = emit;
151
+ const config = init.config;
152
+ this.plan = (0, plan_js_1.buildLoadPlan)(config.executor);
153
+ this.transport = new transport_js_1.LoadTransport({
154
+ keepAlive: config.keepAlive !== false,
155
+ verifySsl: config.verifySsl !== false,
156
+ clientCertificate: config.clientCertificate,
157
+ });
158
+ this.counters = new BigInt64Array(init.shared);
159
+ this.counterStore = (0, exports.createSharedCounterStore)(init.shared, init.counterSlots);
160
+ const userAgent = config.userAgent ?? executeRequest_js_1.DEFAULT_USER_AGENT;
161
+ this.iterationSteps = config.steps.filter((step) => step.phase !== "setup").map((step) => (0, step_compiler_js_1.compileStep)(step, { userAgent }));
162
+ this.baseVars = { ...(config.environment ?? {}), ...init.setupVars };
163
+ this.okRange = config.okStatus ?? DEFAULT_OK;
164
+ this.monitorEnabled = config.monitor?.enabled === true;
165
+ this.monitorEvery = Math.max(1, config.monitor?.sampleEvery ?? 50);
166
+ this.cookieJar = config.cookieJar !== false;
167
+ this.timeoutMs = config.timeoutMs ?? 30_000;
168
+ this.maxRedirects = config.maxRedirects ?? 5;
169
+ this.graceMs = config.gracefulStopMs ?? 5_000;
170
+ this.correct = config.correctCoordinatedOmission !== false && this.plan.kind === "rate";
171
+ this.maxShare = Math.max(1, Math.ceil(this.plan.maxVus / init.count));
172
+ this.datasetRows = init.dataset;
173
+ if (init.dataset?.kind === "fakers") {
174
+ for (const binding of init.dataset.bindings) {
175
+ let expression = binding.generator.trim().replace(/^\{\{\s*|\s*\}\}$/g, "");
176
+ expression = LEGACY_FAKER_ALIASES[expression] ?? expression;
177
+ this.fakerBindings.push({ name: binding.name, compiled: (0, compile_js_1.compileTemplate)(`{{${expression}}}`) });
178
+ }
179
+ }
180
+ this.oauth = {
181
+ cache: this.oauthCache,
182
+ post: async (request) => {
183
+ const result = await this.transport.send({
184
+ url: request.url,
185
+ method: request.method,
186
+ headers: request.headers,
187
+ body: request.body,
188
+ timeoutMs: this.timeoutMs,
189
+ captureBody: true,
190
+ captureOnError: false,
191
+ maxBodyBytes: CAPTURE_BODY_LIMIT,
192
+ maxRedirects: 0,
193
+ signal: this.controller.signal,
194
+ });
195
+ let data = result.bodyText;
196
+ try {
197
+ data = result.bodyText ? JSON.parse(result.bodyText) : null;
198
+ }
199
+ catch {
200
+ // keep text
201
+ }
202
+ return { status: result.status, data };
203
+ },
204
+ };
205
+ }
206
+ /** Start generating load; resolves when this worker has drained and reported `done`. */
207
+ run() {
208
+ this.lag.enable();
209
+ this.startPerf = node_perf_hooks_1.performance.now() + (this.init.startAtEpochMs - Date.now());
210
+ const finished = new Promise((resolve) => {
211
+ this.resolveFinished = resolve;
212
+ });
213
+ this.flushTimer = setInterval(() => this.flush(), FLUSH_INTERVAL_MS);
214
+ if (this.plan.kind === "vus")
215
+ this.scheduleVus();
216
+ else
217
+ this.scheduleArrivals();
218
+ return finished;
219
+ }
220
+ stop(graceMs) {
221
+ this.stopRequested = true;
222
+ this.ending = true;
223
+ this.hardDeadline = node_perf_hooks_1.performance.now() + Math.max(0, graceMs);
224
+ if (!this.controller.signal.aborted)
225
+ setTimeout(() => this.controller.abort(), Math.max(0, graceMs)).unref?.();
226
+ }
227
+ elapsed() {
228
+ return node_perf_hooks_1.performance.now() - this.startPerf;
229
+ }
230
+ // ── Closed model ───────────────────────────────────────────────────────────
231
+ shareOf(total) {
232
+ const whole = Math.round(total);
233
+ const base = Math.floor(whole / this.init.count);
234
+ return base + (this.init.index < whole % this.init.count ? 1 : 0);
235
+ }
236
+ scheduleVus() {
237
+ const tick = () => {
238
+ if (this.finished)
239
+ return;
240
+ const elapsed = this.elapsed();
241
+ if (!this.ending && elapsed >= this.plan.totalDurationMs)
242
+ this.ending = true;
243
+ if (elapsed >= 0 && !this.ending) {
244
+ const wanted = this.shareOf((0, plan_js_1.targetAt)(this.plan, elapsed));
245
+ const active = this.vus.filter((vu) => !vu.exit).length;
246
+ for (let n = active; n < wanted; n += 1)
247
+ this.spawnVu();
248
+ if (active > wanted) {
249
+ let toRetire = active - wanted;
250
+ for (let i = this.vus.length - 1; i >= 0 && toRetire > 0; i -= 1) {
251
+ if (!this.vus[i].exit) {
252
+ this.vus[i].exit = true;
253
+ toRetire -= 1;
254
+ }
255
+ }
256
+ }
257
+ }
258
+ this.checkDrained();
259
+ if (!this.finished)
260
+ this.scheduleTimer = setTimeout(tick, 50);
261
+ };
262
+ this.scheduleTimer = setTimeout(tick, Math.max(0, -this.elapsed()));
263
+ }
264
+ spawnVu() {
265
+ const seq = this.nextVuSeq;
266
+ this.nextVuSeq += 1;
267
+ const vu = {
268
+ id: seq * this.init.count + this.init.index + 1,
269
+ exit: false,
270
+ running: true,
271
+ iterations: 0,
272
+ vars: {},
273
+ cookies: new Map(),
274
+ };
275
+ this.vus.push(vu);
276
+ void this.vuLoop(vu);
277
+ }
278
+ async vuLoop(vu) {
279
+ const think = this.init.config.thinkTimeMs;
280
+ try {
281
+ while (!vu.exit && !this.ending) {
282
+ const index = Number(Atomics.add(this.counters, 0, 1n));
283
+ await this.runIteration(vu, index, null);
284
+ if (think && think.max > 0 && !this.ending && !vu.exit) {
285
+ await sleep(randomBetween(think.min, think.max), this.controller.signal);
286
+ }
287
+ await yieldToEventLoop();
288
+ }
289
+ }
290
+ finally {
291
+ vu.running = false;
292
+ const at = this.vus.indexOf(vu);
293
+ if (at >= 0)
294
+ this.vus.splice(at, 1);
295
+ }
296
+ }
297
+ // ── Open model ─────────────────────────────────────────────────────────────
298
+ scheduleArrivals() {
299
+ const tick = () => {
300
+ if (this.finished)
301
+ return;
302
+ const elapsed = this.elapsed();
303
+ if (!this.ending && elapsed >= this.plan.totalDurationMs) {
304
+ // Issue whatever was due before the end, then stop arriving.
305
+ this.issueDue(this.plan.totalDurationMs);
306
+ this.ending = true;
307
+ }
308
+ if (elapsed >= 0 && !this.ending)
309
+ this.issueDue(elapsed);
310
+ this.checkDrained();
311
+ if (!this.finished)
312
+ this.scheduleTimer = setTimeout(tick, 2);
313
+ };
314
+ this.scheduleTimer = setTimeout(tick, Math.max(0, -this.elapsed()));
315
+ }
316
+ issueDue(elapsed) {
317
+ const { index, count } = this.init;
318
+ const cumulative = (0, plan_js_1.cumulativeIterationsAt)(this.plan, elapsed);
319
+ const due = cumulative >= index + 1 ? Math.floor((cumulative - (index + 1)) / count) + 1 : 0;
320
+ while (this.issued < due) {
321
+ this.issued += 1;
322
+ const globalNumber = (this.issued - 1) * count + index + 1;
323
+ if (this.inflight >= this.maxShare) {
324
+ this.interval.dropped += 1;
325
+ continue;
326
+ }
327
+ const intended = (0, plan_js_1.intendedStartMs)(this.plan, globalNumber);
328
+ const vu = this.freeVus.pop() ?? this.newPooledVu();
329
+ this.inflight += 1;
330
+ const iterationIndex = Number(Atomics.add(this.counters, 0, 1n));
331
+ void this.runIteration(vu, iterationIndex, intended).finally(() => {
332
+ this.inflight -= 1;
333
+ this.freeVus.push(vu);
334
+ });
335
+ }
336
+ }
337
+ newPooledVu() {
338
+ const seq = this.nextVuSeq;
339
+ this.nextVuSeq += 1;
340
+ return { id: seq * this.init.count + this.init.index + 1, exit: false, running: true, iterations: 0, vars: {}, cookies: new Map() };
341
+ }
342
+ // ── Drain ──────────────────────────────────────────────────────────────────
343
+ idle() {
344
+ return this.plan.kind === "vus" ? this.vus.length === 0 : this.inflight === 0;
345
+ }
346
+ checkDrained() {
347
+ if (this.finished)
348
+ return;
349
+ if (this.ending && node_perf_hooks_1.performance.now() > this.hardDeadline)
350
+ this.controller.abort();
351
+ if (this.ending && this.hardDeadline === Number.POSITIVE_INFINITY) {
352
+ // Natural end: in-flight work gets `graceMs` to finish.
353
+ this.hardDeadline = node_perf_hooks_1.performance.now() + this.graceMs;
354
+ setTimeout(() => this.controller.abort(), this.graceMs).unref?.();
355
+ }
356
+ if (this.ending && this.idle())
357
+ this.finish();
358
+ }
359
+ finish() {
360
+ if (this.finished)
361
+ return;
362
+ this.finished = true;
363
+ if (this.scheduleTimer)
364
+ clearTimeout(this.scheduleTimer);
365
+ if (this.flushTimer)
366
+ clearInterval(this.flushTimer);
367
+ this.flush();
368
+ this.flushMonitor();
369
+ this.lag.disable();
370
+ this.transport.destroy();
371
+ this.emit({ t: "done", worker: this.init.index, slowest: this.slowest });
372
+ this.resolveFinished?.();
373
+ }
374
+ // ── Iteration ──────────────────────────────────────────────────────────────
375
+ nextRow(iterationIndex) {
376
+ const dataset = this.datasetRows;
377
+ if (!dataset)
378
+ return {};
379
+ if (dataset.kind === "rows") {
380
+ if (dataset.rows.length === 0)
381
+ return {};
382
+ return dataset.mode === "random"
383
+ ? dataset.rows[Math.floor(Math.random() * dataset.rows.length)]
384
+ : dataset.rows[iterationIndex % dataset.rows.length];
385
+ }
386
+ const row = {};
387
+ const context = (0, template_engine_js_1.createTemplateRuntimeContext)({}, { counterStore: this.counterStore });
388
+ for (const binding of this.fakerBindings)
389
+ row[binding.name] = binding.compiled(context);
390
+ return row;
391
+ }
392
+ async runIteration(vu, iterationIndex, intendedMs) {
393
+ const startedPerf = node_perf_hooks_1.performance.now();
394
+ const delayMs = intendedMs === null ? 0 : Math.max(0, startedPerf - this.startPerf - intendedMs - SCHEDULER_TOLERANCE_MS);
395
+ const row = this.nextRow(iterationIndex);
396
+ const variables = Object.assign({}, this.baseVars, vu.vars, row);
397
+ for (let stepIndex = 0; stepIndex < this.iterationSteps.length; stepIndex += 1) {
398
+ if (this.controller.signal.aborted)
399
+ break;
400
+ const prepared = this.iterationSteps[stepIndex];
401
+ if (prepared.step.firstIterationOnly && vu.iterations > 0)
402
+ continue;
403
+ const outcome = await this.runStep(vu, prepared, variables, row, iterationIndex, delayMs);
404
+ if (!outcome && prepared.step.stopIterationOnFailure)
405
+ break;
406
+ const pause = prepared.step.thinkTimeMs;
407
+ if (pause && pause.max > 0 && stepIndex < this.iterationSteps.length - 1) {
408
+ await sleep(randomBetween(pause.min, pause.max), this.controller.signal);
409
+ }
410
+ }
411
+ vu.iterations += 1;
412
+ if (!this.controller.signal.aborted) {
413
+ const total = node_perf_hooks_1.performance.now() - startedPerf;
414
+ this.interval.iterations += 1;
415
+ this.interval.iteration.record(total);
416
+ if (this.correct)
417
+ this.interval.corrected.record(total + delayMs);
418
+ }
419
+ }
420
+ cookieHeader(vu, url) {
421
+ if (!this.cookieJar)
422
+ return null;
423
+ let host;
424
+ try {
425
+ host = new URL(url).host;
426
+ }
427
+ catch {
428
+ return null;
429
+ }
430
+ const jar = vu.cookies.get(host);
431
+ if (!jar || jar.size === 0)
432
+ return null;
433
+ return [...jar.entries()].map(([name, value]) => `${name}=${value}`).join("; ");
434
+ }
435
+ storeCookies(vu, url, setCookies) {
436
+ if (!this.cookieJar || setCookies.length === 0)
437
+ return;
438
+ let host;
439
+ try {
440
+ host = new URL(url).host;
441
+ }
442
+ catch {
443
+ return;
444
+ }
445
+ let jar = vu.cookies.get(host);
446
+ if (!jar) {
447
+ jar = new Map();
448
+ vu.cookies.set(host, jar);
449
+ }
450
+ for (const cookie of setCookies) {
451
+ const [pair, ...attributes] = cookie.split(";");
452
+ const eq = pair.indexOf("=");
453
+ if (eq <= 0)
454
+ continue;
455
+ const name = pair.slice(0, eq).trim();
456
+ const value = pair.slice(eq + 1).trim();
457
+ const expired = attributes.some((attribute) => /^\s*max-age\s*=\s*0\s*$/i.test(attribute));
458
+ if (expired || value === "")
459
+ jar.delete(name);
460
+ else
461
+ jar.set(name, value);
462
+ }
463
+ }
464
+ /** Returns whether the step counted as a success. */
465
+ async runStep(vu, prepared, variables, row, iterationIndex, delayMs) {
466
+ const step = prepared.step;
467
+ const context = (0, step_compiler_js_1.newTemplateContext)(variables, this.counterStore);
468
+ let request;
469
+ try {
470
+ request = await prepared.build(context, this.oauth);
471
+ }
472
+ catch (error) {
473
+ this.recordFailure(step.id, step.name, "config", error instanceof Error ? error.message : String(error), prepared);
474
+ return false;
475
+ }
476
+ const cookie = this.cookieHeader(vu, request.url);
477
+ if (cookie && !Object.keys(request.headers).some((key) => key.toLowerCase() === "cookie")) {
478
+ request.headers.Cookie = cookie;
479
+ }
480
+ const sampled = this.monitorEnabled && iterationIndex % this.monitorEvery === 0;
481
+ const atMs = this.elapsed();
482
+ const result = await this.transport.send({
483
+ url: request.url,
484
+ method: request.method,
485
+ headers: request.headers,
486
+ body: request.body,
487
+ timeoutMs: step.timeoutMs ?? this.timeoutMs,
488
+ captureBody: prepared.needsBody || sampled,
489
+ captureOnError: this.monitorEnabled,
490
+ maxBodyBytes: prepared.needsBody ? CAPTURE_BODY_LIMIT : MONITOR_BODY_LIMIT,
491
+ maxRedirects: this.maxRedirects,
492
+ signal: this.controller.signal,
493
+ });
494
+ if (result.error?.class === "aborted")
495
+ return false;
496
+ this.storeCookies(vu, request.url, result.setCookies);
497
+ const status = result.status;
498
+ const networkError = result.error !== undefined;
499
+ const ok = !networkError && (prepared.expectOk ? prepared.expectOk(status) : status >= this.okRange.min && status <= this.okRange.max);
500
+ let checksPassed = 0;
501
+ let checksFailed = 0;
502
+ const stepInterval = this.interval.step(step.id);
503
+ if (!networkError && ((step.extract && step.extract.length > 0) || (step.checks && step.checks.length > 0))) {
504
+ const evaluator = (0, flow_eval_js_1.createResponseEvaluator)({
505
+ status,
506
+ durationMs: result.timings.total,
507
+ headers: result.headers,
508
+ bodyText: result.bodyText,
509
+ });
510
+ if (step.extract?.length) {
511
+ evaluator.extract(step.extract, variables);
512
+ if (step.firstIterationOnly)
513
+ for (const extractor of step.extract)
514
+ if (variables[extractor.variable] !== undefined)
515
+ vu.vars[extractor.variable] = variables[extractor.variable];
516
+ }
517
+ if (step.checks?.length) {
518
+ for (const outcome of evaluator.check(step.checks, prepared.resolve((0, step_compiler_js_1.newTemplateContext)(variables, this.counterStore)))) {
519
+ const slot = (stepInterval.checks[outcome.name] ??= { p: 0, f: 0 });
520
+ if (outcome.passed) {
521
+ slot.p += 1;
522
+ checksPassed += 1;
523
+ }
524
+ else {
525
+ slot.f += 1;
526
+ checksFailed += 1;
527
+ }
528
+ }
529
+ }
530
+ }
531
+ else if (networkError && step.checks?.length) {
532
+ for (const check of step.checks) {
533
+ const slot = (stepInterval.checks[check.name] ??= { p: 0, f: 0 });
534
+ slot.f += 1;
535
+ checksFailed += 1;
536
+ }
537
+ }
538
+ this.recordRequest(prepared, result, ok, checksPassed, checksFailed, delayMs, iterationIndex, vu.id);
539
+ if (!ok || sampled) {
540
+ this.captureMonitor(prepared, request, result, ok, iterationIndex, vu.id, atMs, row);
541
+ }
542
+ return ok;
543
+ }
544
+ // ── Recording ──────────────────────────────────────────────────────────────
545
+ recordFailure(stepId, name, errorClass, message, prepared) {
546
+ void name;
547
+ void prepared;
548
+ const interval = this.interval;
549
+ interval.requests += 1;
550
+ interval.failed += 1;
551
+ interval.networkErrors += 1;
552
+ interval.statusClass.c0 += 1;
553
+ interval.statusCodes["0"] = (interval.statusCodes["0"] ?? 0) + 1;
554
+ const step = interval.step(stepId);
555
+ step.requests += 1;
556
+ step.failed += 1;
557
+ step.statusCodes["0"] = (step.statusCodes["0"] ?? 0) + 1;
558
+ this.noteError(errorClass, message);
559
+ }
560
+ noteError(errorClass, message) {
561
+ const current = this.interval.errors.get(errorClass);
562
+ if (current)
563
+ current.count += 1;
564
+ else
565
+ this.interval.errors.set(errorClass, { count: 1, example: message.slice(0, 300) });
566
+ }
567
+ recordRequest(prepared, result, ok, checksPassed, checksFailed, delayMs, iterationIndex, vuId) {
568
+ const interval = this.interval;
569
+ const status = result.status;
570
+ const timings = result.timings;
571
+ interval.requests += 1;
572
+ interval.bytesSent += result.bytesSent;
573
+ interval.bytesReceived += result.bytesReceived;
574
+ interval.checksPassed += checksPassed;
575
+ interval.checksFailed += checksFailed;
576
+ if (result.newConnection)
577
+ interval.newConnections += 1;
578
+ if (timings.blocked > 5)
579
+ interval.blockedRequests += 1;
580
+ interval.duration.record(timings.total);
581
+ interval.waiting.record(timings.waiting);
582
+ interval.receiving.record(timings.receiving);
583
+ interval.sending.record(timings.sending);
584
+ interval.blocked.record(timings.blocked);
585
+ interval.connecting.record(timings.dns + timings.connecting);
586
+ interval.tls.record(timings.tls);
587
+ if (this.correct)
588
+ interval.corrected.record(timings.total + delayMs);
589
+ const code = String(status);
590
+ interval.statusCodes[code] = (interval.statusCodes[code] ?? 0) + 1;
591
+ if (status === 0)
592
+ interval.statusClass.c0 += 1;
593
+ else if (status < 300)
594
+ interval.statusClass.c2 += 1;
595
+ else if (status < 400)
596
+ interval.statusClass.c3 += 1;
597
+ else if (status < 500)
598
+ interval.statusClass.c4 += 1;
599
+ else
600
+ interval.statusClass.c5 += 1;
601
+ const stepInterval = interval.step(prepared.step.id);
602
+ stepInterval.requests += 1;
603
+ stepInterval.checksPassed += checksPassed;
604
+ stepInterval.checksFailed += checksFailed;
605
+ stepInterval.duration.record(timings.total);
606
+ stepInterval.statusCodes[code] = (stepInterval.statusCodes[code] ?? 0) + 1;
607
+ if (!ok) {
608
+ interval.failed += 1;
609
+ stepInterval.failed += 1;
610
+ if (result.error) {
611
+ interval.networkErrors += 1;
612
+ this.noteError(result.error.class, result.error.message);
613
+ }
614
+ else {
615
+ this.noteError(status >= 500 ? "http_5xx" : "http_4xx", `HTTP ${status} from ${prepared.step.name}`);
616
+ }
617
+ }
618
+ this.trackSlowest(prepared, result, status, iterationIndex, vuId);
619
+ }
620
+ trackSlowest(prepared, result, status, iterationIndex, vuId) {
621
+ const duration = result.timings.total;
622
+ if (this.slowest.length >= SLOWEST_KEEP && duration <= this.slowest[this.slowest.length - 1].durationMs)
623
+ return;
624
+ this.slowest.push({
625
+ iterationIndex,
626
+ vuId,
627
+ stepId: prepared.step.id,
628
+ stepName: prepared.step.name,
629
+ atMs: Math.round(this.elapsed()),
630
+ status,
631
+ durationMs: duration,
632
+ ...(result.error ? { errorMessage: result.error.message } : {}),
633
+ });
634
+ this.slowest.sort((a, b) => b.durationMs - a.durationMs);
635
+ if (this.slowest.length > SLOWEST_KEEP)
636
+ this.slowest.length = SLOWEST_KEEP;
637
+ }
638
+ captureMonitor(prepared, request, result, ok, iterationIndex, vuId, atMs, row) {
639
+ void row;
640
+ if (this.monitorBuffer.length >= MONITOR_CAP_PER_INTERVAL) {
641
+ const okAt = this.monitorBuffer.findIndex((entry) => entry.ok);
642
+ if (okAt >= 0)
643
+ this.monitorBuffer.splice(okAt, 1);
644
+ else if (ok)
645
+ return;
646
+ else
647
+ this.monitorBuffer.shift();
648
+ }
649
+ const body = request.bodyPreview;
650
+ const responseBody = result.bodyText ?? "";
651
+ this.monitorBuffer.push({
652
+ id: `${this.init.config.runId}:${this.init.index}:${iterationIndex}:${prepared.step.id}`,
653
+ iterationIndex,
654
+ vuId,
655
+ stepId: prepared.step.id,
656
+ stepName: prepared.step.name,
657
+ atMs: Math.round(atMs),
658
+ method: request.method,
659
+ url: request.url,
660
+ status: result.status,
661
+ ok,
662
+ durationMs: result.timings.total,
663
+ bytesSent: result.bytesSent,
664
+ bytesReceived: result.bytesReceived,
665
+ ...(result.error ? { errorMessage: result.error.message, errorClass: result.error.class } : {}),
666
+ detail: {
667
+ request: {
668
+ headers: Object.entries(request.headers).map(([key, value]) => ({ key, value })),
669
+ query: request.query.map(([key, value]) => ({ key, value })),
670
+ body: body.length > MONITOR_BODY_LIMIT ? body.slice(0, MONITOR_BODY_LIMIT) : body,
671
+ bodyTruncated: body.length > MONITOR_BODY_LIMIT,
672
+ },
673
+ response: {
674
+ headers: Object.entries(result.headers).map(([key, value]) => ({ key, value })),
675
+ body: responseBody.length > MONITOR_BODY_LIMIT ? responseBody.slice(0, MONITOR_BODY_LIMIT) : responseBody,
676
+ bodyTruncated: result.bodyTruncated || responseBody.length > MONITOR_BODY_LIMIT,
677
+ },
678
+ },
679
+ });
680
+ }
681
+ flushMonitor() {
682
+ if (this.monitorBuffer.length === 0)
683
+ return;
684
+ const records = this.monitorBuffer;
685
+ this.monitorBuffer = [];
686
+ this.emit({ t: "monitor", worker: this.init.index, records });
687
+ }
688
+ flush() {
689
+ const interval = this.interval;
690
+ this.interval = new Interval();
691
+ const steps = {};
692
+ for (const [id, entry] of interval.steps) {
693
+ steps[id] = {
694
+ requests: entry.requests,
695
+ failed: entry.failed,
696
+ checksPassed: entry.checksPassed,
697
+ checksFailed: entry.checksFailed,
698
+ checks: entry.checks,
699
+ statusCodes: entry.statusCodes,
700
+ duration: entry.duration.toSparse(),
701
+ };
702
+ }
703
+ const errors = {};
704
+ for (const [key, value] of interval.errors)
705
+ errors[key] = value;
706
+ const lagMean = Number.isFinite(this.lag.mean) ? this.lag.mean / 1e6 : 0;
707
+ const lagMax = Number.isFinite(this.lag.max) ? this.lag.max / 1e6 : 0;
708
+ this.lag.reset();
709
+ const delta = {
710
+ t: "delta",
711
+ worker: this.init.index,
712
+ atMs: Math.round(this.elapsed()),
713
+ activeVus: this.plan.kind === "vus" ? this.vus.length : this.inflight,
714
+ requests: interval.requests,
715
+ failed: interval.failed,
716
+ networkErrors: interval.networkErrors,
717
+ iterations: interval.iterations,
718
+ dropped: interval.dropped,
719
+ bytesSent: interval.bytesSent,
720
+ bytesReceived: interval.bytesReceived,
721
+ checksPassed: interval.checksPassed,
722
+ checksFailed: interval.checksFailed,
723
+ newConnections: interval.newConnections,
724
+ blockedRequests: interval.blockedRequests,
725
+ statusClass: interval.statusClass,
726
+ statusCodes: interval.statusCodes,
727
+ histograms: {
728
+ duration: interval.duration.toSparse(),
729
+ corrected: interval.corrected.toSparse(),
730
+ waiting: interval.waiting.toSparse(),
731
+ receiving: interval.receiving.toSparse(),
732
+ sending: interval.sending.toSparse(),
733
+ blocked: interval.blocked.toSparse(),
734
+ connecting: interval.connecting.toSparse(),
735
+ tls: interval.tls.toSparse(),
736
+ iteration: interval.iteration.toSparse(),
737
+ },
738
+ steps,
739
+ errors,
740
+ loopLagMeanMs: lagMean,
741
+ loopLagMaxMs: lagMax,
742
+ rssBytes: process.memoryUsage().rss,
743
+ };
744
+ this.emit(delta);
745
+ this.flushMonitor();
746
+ }
747
+ }
748
+ exports.LoadWorker = LoadWorker;
749
+ //# sourceMappingURL=worker-core.js.map