@loadstrike/loadstrike-sdk 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/README.md +73 -0
- package/dist/cjs/cluster.js +410 -0
- package/dist/cjs/contracts.js +2 -0
- package/dist/cjs/correlation.js +1009 -0
- package/dist/cjs/index.js +71 -0
- package/dist/cjs/local.js +1884 -0
- package/dist/cjs/package.json +3 -0
- package/dist/cjs/reporting.js +1250 -0
- package/dist/cjs/runtime.js +7013 -0
- package/dist/cjs/sinks.js +2675 -0
- package/dist/cjs/transports.js +3695 -0
- package/dist/esm/cluster.js +403 -0
- package/dist/esm/contracts.js +1 -0
- package/dist/esm/correlation.js +999 -0
- package/dist/esm/index.js +6 -0
- package/dist/esm/local.js +1844 -0
- package/dist/esm/reporting.js +1241 -0
- package/dist/esm/runtime.js +6992 -0
- package/dist/esm/sinks.js +2657 -0
- package/dist/esm/transports.js +3658 -0
- package/dist/types/cluster.d.ts +112 -0
- package/dist/types/contracts.d.ts +439 -0
- package/dist/types/correlation.d.ts +234 -0
- package/dist/types/index.d.ts +13 -0
- package/dist/types/local.d.ts +30 -0
- package/dist/types/reporting.d.ts +6 -0
- package/dist/types/runtime.d.ts +1052 -0
- package/dist/types/sinks.d.ts +497 -0
- package/dist/types/transports.d.ts +745 -0
- package/package.json +110 -0
package/README.md
ADDED
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
# LoadStrike for TypeScript and JavaScript
|
|
2
|
+
|
|
3
|
+
LoadStrike is a TypeScript and JavaScript SDK for defining and running load, traffic-correlation, and reporting scenarios directly inside your application or test process.
|
|
4
|
+
|
|
5
|
+
## Requirements
|
|
6
|
+
|
|
7
|
+
- Node.js 20 or later
|
|
8
|
+
|
|
9
|
+
## Install
|
|
10
|
+
|
|
11
|
+
```bash
|
|
12
|
+
npm install @loadstrike/loadstrike-sdk
|
|
13
|
+
```
|
|
14
|
+
|
|
15
|
+
## What It Provides
|
|
16
|
+
|
|
17
|
+
- Scenario, step, load-simulation, threshold, and metric primitives
|
|
18
|
+
- Native in-process execution with structured run results
|
|
19
|
+
- Local and distributed cluster execution helpers
|
|
20
|
+
- HTML, TXT, CSV, and Markdown report generation
|
|
21
|
+
|
|
22
|
+
## Supported Transports
|
|
23
|
+
|
|
24
|
+
- HTTP
|
|
25
|
+
- Kafka
|
|
26
|
+
- RabbitMQ
|
|
27
|
+
- NATS
|
|
28
|
+
- Redis Streams
|
|
29
|
+
- Azure Event Hubs
|
|
30
|
+
- Push Diffusion
|
|
31
|
+
- Delegate and custom stream endpoints
|
|
32
|
+
|
|
33
|
+
## Supported Reporting Sinks
|
|
34
|
+
|
|
35
|
+
- InfluxDB
|
|
36
|
+
- TimescaleDB
|
|
37
|
+
- Grafana Loki
|
|
38
|
+
- Datadog
|
|
39
|
+
- Splunk HEC
|
|
40
|
+
- OpenTelemetry Collector
|
|
41
|
+
|
|
42
|
+
## Quick Start
|
|
43
|
+
|
|
44
|
+
```ts
|
|
45
|
+
import {
|
|
46
|
+
LoadStrikeResponse,
|
|
47
|
+
LoadStrikeRunner,
|
|
48
|
+
LoadStrikeScenario,
|
|
49
|
+
LoadStrikeSimulation,
|
|
50
|
+
LoadStrikeStep
|
|
51
|
+
} from "@loadstrike/loadstrike-sdk";
|
|
52
|
+
|
|
53
|
+
const scenario = LoadStrikeScenario
|
|
54
|
+
.create("orders", async (context) => {
|
|
55
|
+
return LoadStrikeStep.run(
|
|
56
|
+
"publish-order",
|
|
57
|
+
context,
|
|
58
|
+
async () => LoadStrikeResponse.ok("200")
|
|
59
|
+
);
|
|
60
|
+
})
|
|
61
|
+
.withLoadSimulations(LoadStrikeSimulation.inject(10, 1, 20));
|
|
62
|
+
|
|
63
|
+
const result = await LoadStrikeRunner
|
|
64
|
+
.registerScenarios(scenario)
|
|
65
|
+
.withRunnerKey("rkl_your_runner_key")
|
|
66
|
+
.run();
|
|
67
|
+
```
|
|
68
|
+
|
|
69
|
+
A valid `RunnerKey` is required to execute live workloads.
|
|
70
|
+
|
|
71
|
+
## Documentation
|
|
72
|
+
|
|
73
|
+
https://loadstrike.com/documentation
|
|
@@ -0,0 +1,410 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.DistributedClusterAgent = exports.DistributedClusterCoordinator = exports.LocalClusterCoordinator = void 0;
|
|
4
|
+
exports.planAgentScenarioAssignments = planAgentScenarioAssignments;
|
|
5
|
+
const node_crypto_1 = require("node:crypto");
|
|
6
|
+
const transports_js_1 = require("./transports.js");
|
|
7
|
+
class LocalClusterCoordinator {
|
|
8
|
+
constructor(options = {}) {
|
|
9
|
+
this.options = options;
|
|
10
|
+
}
|
|
11
|
+
async dispatch(agents, scenarioNames, assignmentOptions = {}) {
|
|
12
|
+
const timeoutMs = Math.max(this.options.commandTimeoutMs ?? 120000, 1);
|
|
13
|
+
const retryCount = Math.max(this.options.retryCount ?? 0, 0);
|
|
14
|
+
const assignments = planAgentScenarioAssignments(scenarioNames, agents.length, assignmentOptions);
|
|
15
|
+
const tasks = agents.map((agent, index) => {
|
|
16
|
+
const dispatch = { scenarioNames: assignments[index] ?? [] };
|
|
17
|
+
return this.executeWithRetry(agent, dispatch, timeoutMs, retryCount);
|
|
18
|
+
});
|
|
19
|
+
const nodeResults = await Promise.all(tasks);
|
|
20
|
+
return {
|
|
21
|
+
nodeResults,
|
|
22
|
+
allRequestCount: nodeResults.reduce((sum, x) => sum + x.allRequestCount, 0),
|
|
23
|
+
allOkCount: nodeResults.reduce((sum, x) => sum + x.allOkCount, 0),
|
|
24
|
+
allFailCount: nodeResults.reduce((sum, x) => sum + x.allFailCount, 0),
|
|
25
|
+
failedNodes: nodeResults.filter((x) => !x.success).length
|
|
26
|
+
};
|
|
27
|
+
}
|
|
28
|
+
async executeWithRetry(agent, dispatch, timeoutMs, retryCount) {
|
|
29
|
+
let lastResult = {
|
|
30
|
+
nodeId: agent.nodeId,
|
|
31
|
+
success: false,
|
|
32
|
+
allRequestCount: 0,
|
|
33
|
+
allOkCount: 0,
|
|
34
|
+
allFailCount: 0,
|
|
35
|
+
error: "agent did not execute"
|
|
36
|
+
};
|
|
37
|
+
for (let attempt = 0; attempt <= retryCount; attempt += 1) {
|
|
38
|
+
lastResult = await this.withTimeout(agent.execute(dispatch), timeoutMs, agent.nodeId);
|
|
39
|
+
if (lastResult.success) {
|
|
40
|
+
return lastResult;
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
return lastResult;
|
|
44
|
+
}
|
|
45
|
+
async withTimeout(run, timeoutMs, nodeId) {
|
|
46
|
+
let timeoutHandle;
|
|
47
|
+
const timeout = new Promise((resolve) => {
|
|
48
|
+
timeoutHandle = setTimeout(() => {
|
|
49
|
+
resolve({
|
|
50
|
+
nodeId,
|
|
51
|
+
success: false,
|
|
52
|
+
allRequestCount: 0,
|
|
53
|
+
allOkCount: 0,
|
|
54
|
+
allFailCount: 0,
|
|
55
|
+
error: `node timed out after ${timeoutMs}ms`
|
|
56
|
+
});
|
|
57
|
+
}, timeoutMs);
|
|
58
|
+
});
|
|
59
|
+
const result = await Promise.race([run, timeout]);
|
|
60
|
+
if (timeoutHandle) {
|
|
61
|
+
clearTimeout(timeoutHandle);
|
|
62
|
+
}
|
|
63
|
+
return result;
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
exports.LocalClusterCoordinator = LocalClusterCoordinator;
|
|
67
|
+
function planAgentScenarioAssignments(scenarioNames, agentCount, options = {}) {
|
|
68
|
+
const totalAgents = Math.max(agentCount, 0);
|
|
69
|
+
const assignments = Array.from({ length: totalAgents }, () => []);
|
|
70
|
+
if (totalAgents === 0) {
|
|
71
|
+
return assignments;
|
|
72
|
+
}
|
|
73
|
+
if (options.explicitAssignments?.length) {
|
|
74
|
+
for (let i = 0; i < totalAgents; i += 1) {
|
|
75
|
+
assignments[i] = [...(options.explicitAssignments[i] ?? [])];
|
|
76
|
+
}
|
|
77
|
+
return assignments;
|
|
78
|
+
}
|
|
79
|
+
const selected = new Set((options.selectedScenarioNames ?? scenarioNames).map((x) => String(x)));
|
|
80
|
+
const ordered = scenarioNames.filter((name) => selected.has(name));
|
|
81
|
+
if (!ordered.length) {
|
|
82
|
+
return assignments;
|
|
83
|
+
}
|
|
84
|
+
const weights = Array.from({ length: totalAgents }, (_, i) => {
|
|
85
|
+
const raw = options.agentWeights?.[i];
|
|
86
|
+
if (typeof raw === "number" && Number.isFinite(raw) && raw > 0) {
|
|
87
|
+
return Math.max(Math.trunc(raw), 1);
|
|
88
|
+
}
|
|
89
|
+
return 1;
|
|
90
|
+
});
|
|
91
|
+
const weightedCycle = buildWeightedCycle(weights);
|
|
92
|
+
let cycleIndex = 0;
|
|
93
|
+
const targets = Array.from({ length: totalAgents }, (_, i) => {
|
|
94
|
+
const values = options.agentTargets?.[i];
|
|
95
|
+
if (!values?.length) {
|
|
96
|
+
return null;
|
|
97
|
+
}
|
|
98
|
+
return new Set(values.map((x) => String(x)));
|
|
99
|
+
});
|
|
100
|
+
for (const scenarioName of ordered) {
|
|
101
|
+
const eligible = targets
|
|
102
|
+
.map((target, index) => ({ index, target }))
|
|
103
|
+
.filter((row) => row.target == null || row.target.has(scenarioName))
|
|
104
|
+
.map((row) => row.index);
|
|
105
|
+
if (!eligible.length) {
|
|
106
|
+
continue;
|
|
107
|
+
}
|
|
108
|
+
let selectedAgent = eligible[0];
|
|
109
|
+
for (let attempt = 0; attempt < weightedCycle.length; attempt += 1) {
|
|
110
|
+
const candidate = weightedCycle[(cycleIndex + attempt) % weightedCycle.length];
|
|
111
|
+
if (eligible.includes(candidate)) {
|
|
112
|
+
selectedAgent = candidate;
|
|
113
|
+
cycleIndex = (cycleIndex + attempt + 1) % weightedCycle.length;
|
|
114
|
+
break;
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
assignments[selectedAgent].push(scenarioName);
|
|
118
|
+
}
|
|
119
|
+
return assignments;
|
|
120
|
+
}
|
|
121
|
+
class DistributedClusterCoordinator {
|
|
122
|
+
constructor(options) {
|
|
123
|
+
this.options = options;
|
|
124
|
+
}
|
|
125
|
+
async dispatch(assignments) {
|
|
126
|
+
const expected = Math.max(this.options.expectedAgentResults, 0);
|
|
127
|
+
const timeoutMs = Math.max(this.options.commandTimeoutMs ?? 120000, 1);
|
|
128
|
+
const runSubject = this.buildRunSubject();
|
|
129
|
+
const replySubject = this.buildReplySubject();
|
|
130
|
+
const commandProducer = transports_js_1.EndpointAdapterFactory.create(this.natsEndpoint("Produce", "coordinator-command", runSubject));
|
|
131
|
+
const resultConsumer = transports_js_1.EndpointAdapterFactory.create(this.natsEndpoint("Consume", "coordinator-result", replySubject));
|
|
132
|
+
try {
|
|
133
|
+
// Prime the reply subscription before commands are published because core NATS subjects do not retain messages.
|
|
134
|
+
await resultConsumer.consume();
|
|
135
|
+
for (let i = 0; i < assignments.length; i += 1) {
|
|
136
|
+
const commandId = (0, node_crypto_1.randomUUID)().replace(/-/g, "");
|
|
137
|
+
const command = {
|
|
138
|
+
commandId,
|
|
139
|
+
sessionId: this.options.sessionId,
|
|
140
|
+
testSuite: this.options.testSuite,
|
|
141
|
+
testName: this.options.testName,
|
|
142
|
+
agentIndex: i,
|
|
143
|
+
agentCount: expected,
|
|
144
|
+
targetScenarios: assignments[i] ?? [],
|
|
145
|
+
replySubject
|
|
146
|
+
};
|
|
147
|
+
await commandProducer.produce({
|
|
148
|
+
headers: {
|
|
149
|
+
"x-cluster-command-id": commandId,
|
|
150
|
+
"x-cluster-reply-subject": replySubject
|
|
151
|
+
},
|
|
152
|
+
body: command
|
|
153
|
+
});
|
|
154
|
+
}
|
|
155
|
+
const byCommand = new Map();
|
|
156
|
+
const deadline = Date.now() + timeoutMs;
|
|
157
|
+
while (byCommand.size < expected && Date.now() < deadline) {
|
|
158
|
+
const payload = await resultConsumer.consume();
|
|
159
|
+
if (!payload?.body || typeof payload.body !== "object" || Array.isArray(payload.body)) {
|
|
160
|
+
await sleep(5);
|
|
161
|
+
continue;
|
|
162
|
+
}
|
|
163
|
+
const result = parseRunResult(payload.body);
|
|
164
|
+
if (!result.commandId) {
|
|
165
|
+
continue;
|
|
166
|
+
}
|
|
167
|
+
byCommand.set(result.commandId, result);
|
|
168
|
+
}
|
|
169
|
+
const nodeResults = Array.from(byCommand.values()).map((result) => convertRunResult(result));
|
|
170
|
+
return {
|
|
171
|
+
nodeResults,
|
|
172
|
+
allRequestCount: nodeResults.reduce((sum, x) => sum + x.allRequestCount, 0),
|
|
173
|
+
allOkCount: nodeResults.reduce((sum, x) => sum + x.allOkCount, 0),
|
|
174
|
+
allFailCount: nodeResults.reduce((sum, x) => sum + x.allFailCount, 0),
|
|
175
|
+
failedNodes: nodeResults.filter((x) => !x.success).length + Math.max(expected - nodeResults.length, 0),
|
|
176
|
+
missingNodes: Math.max(expected - nodeResults.length, 0),
|
|
177
|
+
runSubject,
|
|
178
|
+
replySubject
|
|
179
|
+
};
|
|
180
|
+
}
|
|
181
|
+
finally {
|
|
182
|
+
await resultConsumer.dispose?.().catch(() => { });
|
|
183
|
+
await commandProducer.dispose?.().catch(() => { });
|
|
184
|
+
}
|
|
185
|
+
}
|
|
186
|
+
buildRunSubject() {
|
|
187
|
+
return `loadstrike.${sanitizeToken(this.options.clusterId)}.${sanitizeToken(this.options.agentGroup ?? "default")}.run`;
|
|
188
|
+
}
|
|
189
|
+
buildReplySubject() {
|
|
190
|
+
return `loadstrike.${sanitizeToken(this.options.clusterId)}.${sanitizeToken(this.options.sessionId)}.reply.${(0, node_crypto_1.randomUUID)().replace(/-/g, "")}`;
|
|
191
|
+
}
|
|
192
|
+
natsEndpoint(mode, name, subject) {
|
|
193
|
+
return {
|
|
194
|
+
kind: "Nats",
|
|
195
|
+
mode,
|
|
196
|
+
name,
|
|
197
|
+
trackingField: "header:x-cluster-command-id",
|
|
198
|
+
nats: {
|
|
199
|
+
...(this.options.nats ?? {}),
|
|
200
|
+
Subject: subject,
|
|
201
|
+
StartFromEarliest: true
|
|
202
|
+
}
|
|
203
|
+
};
|
|
204
|
+
}
|
|
205
|
+
}
|
|
206
|
+
exports.DistributedClusterCoordinator = DistributedClusterCoordinator;
|
|
207
|
+
class DistributedClusterAgent {
|
|
208
|
+
constructor(options) {
|
|
209
|
+
this.commandConsumer = null;
|
|
210
|
+
this.options = options;
|
|
211
|
+
}
|
|
212
|
+
async dispose() {
|
|
213
|
+
await this.commandConsumer?.dispose?.().catch(() => { });
|
|
214
|
+
this.commandConsumer = null;
|
|
215
|
+
}
|
|
216
|
+
async pollAndExecuteOnce(execute) {
|
|
217
|
+
const runSubject = `loadstrike.${sanitizeToken(this.options.clusterId)}.${sanitizeToken(this.options.agentGroup ?? "default")}.run`;
|
|
218
|
+
const queueGroup = `loadstrike.${sanitizeToken(this.options.clusterId)}.${sanitizeToken(this.options.agentGroup ?? "default")}.agents`;
|
|
219
|
+
if (!this.commandConsumer) {
|
|
220
|
+
this.commandConsumer = transports_js_1.EndpointAdapterFactory.create({
|
|
221
|
+
kind: "Nats",
|
|
222
|
+
mode: "Consume",
|
|
223
|
+
name: `agent-${this.options.agentId}`,
|
|
224
|
+
trackingField: "header:x-cluster-command-id",
|
|
225
|
+
nats: {
|
|
226
|
+
...(this.options.nats ?? {}),
|
|
227
|
+
Subject: runSubject,
|
|
228
|
+
QueueGroup: queueGroup,
|
|
229
|
+
StartFromEarliest: true
|
|
230
|
+
}
|
|
231
|
+
});
|
|
232
|
+
}
|
|
233
|
+
const payload = await this.commandConsumer.consume();
|
|
234
|
+
if (!payload?.body || typeof payload.body !== "object" || Array.isArray(payload.body)) {
|
|
235
|
+
return false;
|
|
236
|
+
}
|
|
237
|
+
const command = parseRunCommand(payload.body);
|
|
238
|
+
if (!command.commandId) {
|
|
239
|
+
return false;
|
|
240
|
+
}
|
|
241
|
+
const replySubject = stringOrDefault(payload.headers?.["x-cluster-reply-subject"], command.replySubject ?? "");
|
|
242
|
+
const resultProducer = replySubject
|
|
243
|
+
? transports_js_1.EndpointAdapterFactory.create({
|
|
244
|
+
kind: "Nats",
|
|
245
|
+
mode: "Produce",
|
|
246
|
+
name: `agent-${this.options.agentId}-result`,
|
|
247
|
+
trackingField: "header:x-cluster-command-id",
|
|
248
|
+
nats: {
|
|
249
|
+
...(this.options.nats ?? {}),
|
|
250
|
+
Subject: replySubject
|
|
251
|
+
}
|
|
252
|
+
})
|
|
253
|
+
: null;
|
|
254
|
+
try {
|
|
255
|
+
let result;
|
|
256
|
+
try {
|
|
257
|
+
const nodeResult = await execute({ scenarioNames: command.targetScenarios });
|
|
258
|
+
result = {
|
|
259
|
+
commandId: command.commandId,
|
|
260
|
+
agentId: this.options.agentId,
|
|
261
|
+
isSuccess: nodeResult.success,
|
|
262
|
+
errorMessage: nodeResult.error,
|
|
263
|
+
stats: nodeResult.stats ?? {
|
|
264
|
+
allRequestCount: nodeResult.allRequestCount,
|
|
265
|
+
allOkCount: nodeResult.allOkCount,
|
|
266
|
+
allFailCount: nodeResult.allFailCount
|
|
267
|
+
}
|
|
268
|
+
};
|
|
269
|
+
}
|
|
270
|
+
catch (error) {
|
|
271
|
+
result = {
|
|
272
|
+
commandId: command.commandId,
|
|
273
|
+
agentId: this.options.agentId,
|
|
274
|
+
isSuccess: false,
|
|
275
|
+
errorMessage: String(error ?? "agent execution failed")
|
|
276
|
+
};
|
|
277
|
+
}
|
|
278
|
+
if (resultProducer) {
|
|
279
|
+
await resultProducer.produce({
|
|
280
|
+
headers: {
|
|
281
|
+
"x-cluster-command-id": result.commandId
|
|
282
|
+
},
|
|
283
|
+
body: result
|
|
284
|
+
});
|
|
285
|
+
}
|
|
286
|
+
return true;
|
|
287
|
+
}
|
|
288
|
+
finally {
|
|
289
|
+
await resultProducer?.dispose?.().catch(() => { });
|
|
290
|
+
}
|
|
291
|
+
}
|
|
292
|
+
}
|
|
293
|
+
exports.DistributedClusterAgent = DistributedClusterAgent;
|
|
294
|
+
function parseRunCommand(value) {
|
|
295
|
+
const targetScenarios = Array.isArray(value.targetScenarios)
|
|
296
|
+
? value.targetScenarios.map((x) => String(x))
|
|
297
|
+
: Array.isArray(value.TargetScenarios)
|
|
298
|
+
? value.TargetScenarios.map((x) => String(x))
|
|
299
|
+
: [];
|
|
300
|
+
return {
|
|
301
|
+
commandId: stringOrDefault(value.commandId, stringOrDefault(value.CommandId, "")),
|
|
302
|
+
sessionId: stringOrDefault(value.sessionId, stringOrDefault(value.SessionId, "")),
|
|
303
|
+
testSuite: stringOrDefault(value.testSuite, stringOrDefault(value.TestSuite, "")),
|
|
304
|
+
testName: stringOrDefault(value.testName, stringOrDefault(value.TestName, "")),
|
|
305
|
+
agentIndex: numberOrDefault(value.agentIndex, numberOrDefault(value.AgentIndex, 0)),
|
|
306
|
+
agentCount: numberOrDefault(value.agentCount, numberOrDefault(value.AgentCount, 0)),
|
|
307
|
+
targetScenarios,
|
|
308
|
+
replySubject: stringOrDefault(value.replySubject, stringOrDefault(value.ReplySubject, ""))
|
|
309
|
+
};
|
|
310
|
+
}
|
|
311
|
+
function parseRunResult(value) {
|
|
312
|
+
const statsValue = value.stats ?? value.Stats;
|
|
313
|
+
const statsRecord = statsValue && typeof statsValue === "object" && !Array.isArray(statsValue)
|
|
314
|
+
? statsValue
|
|
315
|
+
: null;
|
|
316
|
+
return {
|
|
317
|
+
commandId: stringOrDefault(value.commandId, stringOrDefault(value.CommandId, "")),
|
|
318
|
+
agentId: stringOrDefault(value.agentId, stringOrDefault(value.AgentId, "")),
|
|
319
|
+
isSuccess: booleanOrDefault(value.isSuccess, booleanOrDefault(value.IsSuccess, false)),
|
|
320
|
+
errorMessage: stringOrDefault(value.errorMessage, stringOrDefault(value.ErrorMessage, "")),
|
|
321
|
+
stats: statsRecord
|
|
322
|
+
? {
|
|
323
|
+
allBytes: numberOrDefault(statsRecord.allBytes, numberOrDefault(statsRecord.AllBytes, 0)),
|
|
324
|
+
allRequestCount: numberOrDefault(statsRecord.allRequestCount, numberOrDefault(statsRecord.AllRequestCount, 0)),
|
|
325
|
+
allOkCount: numberOrDefault(statsRecord.allOkCount, numberOrDefault(statsRecord.AllOkCount, 0)),
|
|
326
|
+
allFailCount: numberOrDefault(statsRecord.allFailCount, numberOrDefault(statsRecord.AllFailCount, 0)),
|
|
327
|
+
durationMs: numberOrDefault(statsRecord.durationMs, numberOrDefault(statsRecord.DurationMs, 0)),
|
|
328
|
+
metrics: recordOrUndefined(statsRecord.metrics ?? statsRecord.Metrics),
|
|
329
|
+
scenarioStats: arrayOrUndefined(statsRecord.scenarioStats ?? statsRecord.ScenarioStats),
|
|
330
|
+
thresholds: arrayOrUndefined(statsRecord.thresholds ?? statsRecord.Thresholds),
|
|
331
|
+
pluginsData: arrayOrUndefined(statsRecord.pluginsData ?? statsRecord.PluginsData),
|
|
332
|
+
nodeInfo: recordOrUndefined(statsRecord.nodeInfo ?? statsRecord.NodeInfo),
|
|
333
|
+
testInfo: recordOrUndefined(statsRecord.testInfo ?? statsRecord.TestInfo)
|
|
334
|
+
}
|
|
335
|
+
: undefined
|
|
336
|
+
};
|
|
337
|
+
}
|
|
338
|
+
function convertRunResult(result) {
|
|
339
|
+
return {
|
|
340
|
+
nodeId: result.agentId || "unknown-agent",
|
|
341
|
+
success: result.isSuccess,
|
|
342
|
+
allRequestCount: result.stats?.allRequestCount ?? 0,
|
|
343
|
+
allOkCount: result.stats?.allOkCount ?? 0,
|
|
344
|
+
allFailCount: result.stats?.allFailCount ?? 0,
|
|
345
|
+
stats: result.stats,
|
|
346
|
+
error: result.errorMessage ?? ""
|
|
347
|
+
};
|
|
348
|
+
}
|
|
349
|
+
function sanitizeToken(value) {
|
|
350
|
+
if (!value.trim()) {
|
|
351
|
+
return "default";
|
|
352
|
+
}
|
|
353
|
+
return value.replace(/[^a-zA-Z0-9\-_]/g, "_");
|
|
354
|
+
}
|
|
355
|
+
function buildWeightedCycle(weights) {
|
|
356
|
+
const cycle = [];
|
|
357
|
+
for (let i = 0; i < weights.length; i += 1) {
|
|
358
|
+
for (let repeat = 0; repeat < Math.max(weights[i], 1); repeat += 1) {
|
|
359
|
+
cycle.push(i);
|
|
360
|
+
}
|
|
361
|
+
}
|
|
362
|
+
return cycle.length ? cycle : [0];
|
|
363
|
+
}
|
|
364
|
+
function stringOrDefault(value, fallback) {
|
|
365
|
+
if (typeof value === "string" && value.trim()) {
|
|
366
|
+
return value;
|
|
367
|
+
}
|
|
368
|
+
return fallback;
|
|
369
|
+
}
|
|
370
|
+
function numberOrDefault(value, fallback) {
|
|
371
|
+
if (typeof value === "number" && Number.isFinite(value)) {
|
|
372
|
+
return value;
|
|
373
|
+
}
|
|
374
|
+
if (typeof value === "string" && value.trim()) {
|
|
375
|
+
const parsed = Number(value);
|
|
376
|
+
if (Number.isFinite(parsed)) {
|
|
377
|
+
return parsed;
|
|
378
|
+
}
|
|
379
|
+
}
|
|
380
|
+
return fallback;
|
|
381
|
+
}
|
|
382
|
+
function recordOrUndefined(value) {
|
|
383
|
+
return value && typeof value === "object" && !Array.isArray(value)
|
|
384
|
+
? value
|
|
385
|
+
: undefined;
|
|
386
|
+
}
|
|
387
|
+
function arrayOrUndefined(value) {
|
|
388
|
+
return Array.isArray(value) ? value : undefined;
|
|
389
|
+
}
|
|
390
|
+
function booleanOrDefault(value, fallback) {
|
|
391
|
+
if (typeof value === "boolean") {
|
|
392
|
+
return value;
|
|
393
|
+
}
|
|
394
|
+
if (typeof value === "number") {
|
|
395
|
+
return value !== 0;
|
|
396
|
+
}
|
|
397
|
+
if (typeof value === "string" && value.trim()) {
|
|
398
|
+
const normalized = value.trim().toLowerCase();
|
|
399
|
+
if (normalized === "true" || normalized === "1" || normalized === "yes") {
|
|
400
|
+
return true;
|
|
401
|
+
}
|
|
402
|
+
if (normalized === "false" || normalized === "0" || normalized === "no") {
|
|
403
|
+
return false;
|
|
404
|
+
}
|
|
405
|
+
}
|
|
406
|
+
return fallback;
|
|
407
|
+
}
|
|
408
|
+
async function sleep(ms) {
|
|
409
|
+
await new Promise((resolve) => setTimeout(resolve, ms));
|
|
410
|
+
}
|