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