@loadstrike/loadstrike-sdk 1.0.31001 → 1.0.32601

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 (45) hide show
  1. package/README.md +16 -2
  2. package/dist/cjs/internal/prometheus-remote-write.js +37 -0
  3. package/dist/cjs/internal/reporting-sink-http-error.js +17 -0
  4. package/dist/cjs/internal/vendor-metric-payloads.js +390 -0
  5. package/dist/cjs/iteration-observations.js +24 -8
  6. package/dist/cjs/local-report-input.js +21 -0
  7. package/dist/cjs/local.js +48 -63
  8. package/dist/cjs/report-history.js +421 -0
  9. package/dist/cjs/reporting-containment.js +242 -0
  10. package/dist/cjs/reporting-svg.js +116 -0
  11. package/dist/cjs/reporting.js +413 -136
  12. package/dist/cjs/runtime.js +237 -8
  13. package/dist/cjs/sinks.js +1337 -38
  14. package/dist/cjs/transports.js +1339 -151
  15. package/dist/esm/internal/prometheus-remote-write.js +31 -0
  16. package/dist/esm/internal/reporting-sink-http-error.js +13 -0
  17. package/dist/esm/internal/vendor-metric-payloads.js +382 -0
  18. package/dist/esm/iteration-observations.js +24 -8
  19. package/dist/esm/local-report-input.js +17 -0
  20. package/dist/esm/local.js +49 -64
  21. package/dist/esm/report-history.js +413 -0
  22. package/dist/esm/reporting-containment.js +238 -0
  23. package/dist/esm/reporting-svg.js +113 -0
  24. package/dist/esm/reporting.js +413 -136
  25. package/dist/esm/runtime.js +239 -10
  26. package/dist/esm/sinks.js +1334 -35
  27. package/dist/esm/transports.js +1335 -151
  28. package/dist/types/contracts.d.ts +1 -0
  29. package/dist/types/index.d.ts +1 -1
  30. package/dist/types/internal/prometheus-remote-write.d.ts +2 -0
  31. package/dist/types/internal/reporting-sink-http-error.d.ts +6 -0
  32. package/dist/types/internal/vendor-metric-payloads.d.ts +48 -0
  33. package/dist/types/local-report-input.d.ts +6 -0
  34. package/dist/types/local.d.ts +0 -6
  35. package/dist/types/report-history.d.ts +124 -0
  36. package/dist/types/reporting-containment.d.ts +2 -0
  37. package/dist/types/reporting-svg.d.ts +2 -0
  38. package/dist/types/reporting.d.ts +6 -3
  39. package/dist/types/runtime.d.ts +24 -0
  40. package/dist/types/sinks.d.ts +134 -17
  41. package/dist/types/transports.d.ts +2 -0
  42. package/package.json +9 -3
  43. package/dist/cjs/internal-build.js +0 -4
  44. package/dist/esm/internal-build.js +0 -1
  45. package/dist/types/internal-build.d.ts +0 -1
package/dist/esm/local.js CHANGED
@@ -15,10 +15,9 @@ import * as fs from "node:fs";
15
15
  import * as childProcess from "node:child_process";
16
16
  import { createHash, createVerify, randomUUID } from "node:crypto";
17
17
  import { CorrelationStoreConfiguration, CrossPlatformTrackingRuntime, RedisCorrelationStoreOptions, RedisCorrelationStore, TrackingFieldSelector } from "./correlation.js";
18
- import { EndpointAdapterFactory, LOADSTRIKE_TRACE_ID_TRACKING_FIELD } from "./transports.js";
19
- import { BLACKBOX_LICENSING_API_BASE_URL_TOKEN } from "./internal-build.js";
18
+ import { EndpointAdapterFactory, LOADSTRIKE_TRACE_ID_TRACKING_FIELD, validateNativeEndpointExecutionSupport } from "./transports.js";
19
+ import { assertNoUnsupportedReportingInfraConfig, assertNoUnsupportedReportingSinkGraph } from "./reporting-containment.js";
20
20
  const DEFAULT_LICENSING_API_BASE_URL = "https://licensing.loadstrike.com";
21
- let developmentLicensingApiBaseUrlOverride;
22
21
  const BUILT_IN_WORKER_PLUGIN_NAMES = new Set([
23
22
  "loadstrike failed responses",
24
23
  "loadstrike correlation"
@@ -72,7 +71,7 @@ export class LoadStrikeLocalClient {
72
71
  _LoadStrikeLocalClient_signingKeyCache.set(this, new Map());
73
72
  _LoadStrikeLocalClient_heartbeatDrains.set(this, new WeakMap());
74
73
  assertNoDisableLicenseEnforcementOption(options, "LoadStrikeLocalClient");
75
- __classPrivateFieldSet(this, _LoadStrikeLocalClient_licensingApiBaseUrl, resolveLicensingApiBaseUrl(), "f");
74
+ __classPrivateFieldSet(this, _LoadStrikeLocalClient_licensingApiBaseUrl, DEFAULT_LICENSING_API_BASE_URL, "f");
76
75
  this.licenseValidationTimeoutMs = normalizeTimeoutMs(options.licenseValidationTimeoutMs);
77
76
  }
78
77
  portalReportingIngestUrl() {
@@ -80,6 +79,7 @@ export class LoadStrikeLocalClient {
80
79
  }
81
80
  async run(request) {
82
81
  const sanitized = sanitizeRequest(request);
82
+ validateContainmentRequest(sanitized);
83
83
  const licenseSession = await this.acquireLicenseLease(sanitized);
84
84
  try {
85
85
  const createdUtc = new Date();
@@ -534,66 +534,27 @@ function sanitizeRequest(request) {
534
534
  RunArgs: runArgs
535
535
  };
536
536
  }
537
- function normalizeLicensingApiBaseUrl(value) {
538
- const normalized = (value ?? "").trim();
539
- return normalized || DEFAULT_LICENSING_API_BASE_URL;
540
- }
541
- function resolveLicensingApiBaseUrl() {
542
- return normalizeLicensingApiBaseUrl(developmentLicensingApiBaseUrlOverride ?? resolveInternalBlackBoxLicensingApiBaseUrl());
543
- }
544
- function setDevelopmentLicensingApiBaseUrlOverride(value) {
545
- if (value != null && value.trim()) {
546
- assertInternalTestHarnessLicensingOverride();
547
- const normalized = normalizeLicensingApiBaseUrl(value);
548
- if (!isLoopbackHttpBaseUrl(normalized)) {
549
- throw new TypeError("Internal development licensing API overrides must use a loopback http URL.");
537
+ function validateContainmentRequest(request) {
538
+ const scenarios = Array.isArray(request.Scenarios) ? request.Scenarios : [];
539
+ for (const scenarioValue of scenarios) {
540
+ const scenario = asRecord(scenarioValue);
541
+ const tracking = asRecord(pickValue(scenario, "Tracking", "tracking"));
542
+ for (const field of ["Source", "Destination"]) {
543
+ const endpoint = asRecord(pickValue(tracking, field, field.toLowerCase()));
544
+ if (Object.keys(endpoint).length > 0) {
545
+ validateNativeEndpointExecutionSupport(endpoint);
546
+ }
550
547
  }
551
- developmentLicensingApiBaseUrlOverride = normalized;
552
- return;
553
- }
554
- if (value != null) {
555
- assertInternalTestHarnessLicensingOverride();
556
- }
557
- developmentLicensingApiBaseUrlOverride = undefined;
558
- }
559
- function assertInternalTestHarnessLicensingOverride() {
560
- if (isInternalTestHarnessCall()) {
561
- return;
562
- }
563
- throw new TypeError("Internal development licensing API override is available only to LoadStrike SDK tests.");
564
- }
565
- function isInternalTestHarnessCall() {
566
- const stack = String(new Error().stack ?? "").replace(/\\/g, "/").toLowerCase();
567
- return stack.includes("/sdk/ts/tests/");
568
- }
569
- function isLoopbackHttpBaseUrl(value) {
570
- let parsed;
571
- try {
572
- parsed = new URL(value);
573
- }
574
- catch {
575
- return false;
576
- }
577
- if (parsed.protocol !== "http:") {
578
- return false;
579
548
  }
580
- const host = parsed.hostname.trim().toLowerCase();
581
- return host === "localhost" || host === "127.0.0.1" || host === "[::1]" || host === "::1" || host.endsWith(".localhost");
582
- }
583
- function resolveInternalBlackBoxLicensingApiBaseUrl() {
584
- const compiledToken = String(BLACKBOX_LICENSING_API_BASE_URL_TOKEN ?? "").trim();
585
- if (!compiledToken) {
586
- return undefined;
587
- }
588
- const envToken = String(process.env.LOADSTRIKE_INTERNAL_BLACKBOX_API_BASE_URL_TOKEN ?? "").trim();
589
- if (!envToken || envToken !== compiledToken) {
590
- return undefined;
591
- }
592
- const candidate = normalizeLicensingApiBaseUrl(process.env.LOADSTRIKE_INTERNAL_BLACKBOX_API_BASE_URL);
593
- if (!isLoopbackHttpBaseUrl(candidate)) {
594
- return undefined;
549
+ const context = asRecord(request.Context);
550
+ const reportingSinks = asList(pickValue(context, "ReportingSinks", "reportingSinks"));
551
+ for (const [key, value] of Object.entries(context)) {
552
+ const normalizedKey = key.trim().toLowerCase().replace(/[^a-z0-9]+/g, "");
553
+ if (normalizedKey === "infraconfig") {
554
+ assertNoUnsupportedReportingInfraConfig(value, reportingSinks);
555
+ }
595
556
  }
596
- return candidate;
557
+ assertNoUnsupportedReportingSinkGraph(reportingSinks);
597
558
  }
598
559
  function normalizeTimeoutMs(value) {
599
560
  if (typeof value !== "number" || !Number.isFinite(value) || value <= 0) {
@@ -1301,6 +1262,20 @@ function mapEndpointSpec(spec, useLoadStrikeTraceIdHeader = false) {
1301
1262
  redisStreams: asRecord(pickValue(spec, "RedisStreams", "redisStreams")),
1302
1263
  azureEventHubs: asRecord(pickValue(spec, "AzureEventHubs", "azureEventHubs")),
1303
1264
  pushDiffusion: asRecord(pickValue(spec, "PushDiffusion", "pushDiffusion")),
1265
+ grpc: mapEndpointProtocolOptions(spec, ["Grpc", "grpc"], [
1266
+ "Target", "target", "ServiceName", "serviceName", "MethodName", "methodName",
1267
+ "MethodType", "methodType", "Deadline", "deadline", "DeadlineSeconds", "deadlineSeconds",
1268
+ "DeadlineMs", "deadlineMs", "Metadata", "metadata", "Produce", "produce", "Consume", "consume",
1269
+ "ProduceAsync", "produceAsync", "ConsumeAsync", "consumeAsync", "ConnectionMetadata", "connectionMetadata",
1270
+ "NativeClient", "nativeClient"
1271
+ ]),
1272
+ webSocket: mapEndpointProtocolOptions(spec, ["WebSocket", "webSocket"], [
1273
+ "Url", "url", "Subprotocols", "subprotocols", "ConnectTimeout", "connectTimeout",
1274
+ "ConnectTimeoutSeconds", "connectTimeoutSeconds", "ConnectTimeoutMs", "connectTimeoutMs",
1275
+ "CloseTimeout", "closeTimeout", "CloseTimeoutSeconds", "closeTimeoutSeconds", "CloseTimeoutMs", "closeTimeoutMs",
1276
+ "Produce", "produce", "Consume", "consume", "ProduceAsync", "produceAsync", "ConsumeAsync", "consumeAsync",
1277
+ "ConnectionMetadata", "connectionMetadata", "NativeClient", "nativeClient"
1278
+ ]),
1304
1279
  delegate: (typeof delegateProduce === "function"
1305
1280
  || typeof delegateConsume === "function"
1306
1281
  || typeof delegateProduceAsync === "function"
@@ -1323,6 +1298,19 @@ function mapEndpointSpec(spec, useLoadStrikeTraceIdHeader = false) {
1323
1298
  : undefined
1324
1299
  };
1325
1300
  }
1301
+ function mapEndpointProtocolOptions(spec, nestedKeys, flatKeys) {
1302
+ const nested = asRecord(pickValue(spec, ...nestedKeys));
1303
+ if (Object.keys(nested).length > 0) {
1304
+ return nested;
1305
+ }
1306
+ const options = {};
1307
+ for (const key of flatKeys) {
1308
+ if (Object.prototype.hasOwnProperty.call(spec, key)) {
1309
+ options[key] = spec[key];
1310
+ }
1311
+ }
1312
+ return options;
1313
+ }
1326
1314
  function normalizePayload(payload, endpoint, index) {
1327
1315
  const resolvedBody = payload?.body ?? endpoint.messagePayload;
1328
1316
  const normalized = {
@@ -2279,8 +2267,6 @@ export const __loadstrikeTestExports = {
2279
2267
  mapEnvironmentClassification,
2280
2268
  mapEndpointSpec,
2281
2269
  normalizeEndpointPayloadValue,
2282
- normalizeLicensingApiBaseUrl,
2283
- resolveLicensingApiBaseUrl,
2284
2270
  normalizeNodeType,
2285
2271
  normalizePayload,
2286
2272
  normalizeStringArray,
@@ -2299,7 +2285,6 @@ export const __loadstrikeTestExports = {
2299
2285
  sanitizeLooseJson,
2300
2286
  sanitizeRequest,
2301
2287
  setJsonPathValue,
2302
- setDevelopmentLicensingApiBaseUrlOverride,
2303
2288
  sleep,
2304
2289
  stringOrDefault,
2305
2290
  toBoolean,
@@ -0,0 +1,413 @@
1
+ export const REPORT_HISTORY_MAX_POINTS = 2048;
2
+ export const REPORT_HISTORY_MAX_SCALAR_VALUES = 262144;
3
+ /**
4
+ * Bounded report-only cumulative telemetry. This type is intentionally not
5
+ * exported from the package entry point and never participates in public run,
6
+ * sink, observation, or cluster payloads.
7
+ */
8
+ export class ReportHistoryCollector {
9
+ constructor(options) {
10
+ this.points = [];
11
+ this.terminalCaptured = false;
12
+ const scenarioCount = Math.max(1, Math.trunc(options.scenarioCount));
13
+ const scalarValuesPerPoint = scenarioCount * 16;
14
+ const budgetPointLimit = Math.floor(REPORT_HISTORY_MAX_SCALAR_VALUES / scalarValuesPerPoint);
15
+ if (!Number.isSafeInteger(scalarValuesPerPoint) || budgetPointLimit < 2) {
16
+ this.pointLimit = 0;
17
+ this.reasonCategory = "budget_pressure";
18
+ }
19
+ else {
20
+ const requestedLimit = options.maxPoints === undefined
21
+ ? REPORT_HISTORY_MAX_POINTS
22
+ : Math.max(2, Math.trunc(options.maxPoints));
23
+ this.pointLimit = Math.min(REPORT_HISTORY_MAX_POINTS, requestedLimit, Math.max(2, budgetPointLimit));
24
+ }
25
+ this.nowNs = options.nowNs ?? (() => process.hrtime.bigint());
26
+ this.onCaptureError = options.onCaptureError;
27
+ }
28
+ get available() {
29
+ return this.reasonCategory === undefined;
30
+ }
31
+ start(startNs = this.nowNs()) {
32
+ if (!this.available || this.startNs !== undefined) {
33
+ return;
34
+ }
35
+ this.startNs = startNs;
36
+ }
37
+ capture(snapshotFactory) {
38
+ return this.captureSafely(snapshotFactory, false);
39
+ }
40
+ finalize(snapshotFactory) {
41
+ return this.captureSafely(snapshotFactory, true);
42
+ }
43
+ disable(reasonCategory) {
44
+ this.reasonCategory = reasonCategory;
45
+ this.points = [];
46
+ }
47
+ toProjection() {
48
+ if (this.reasonCategory !== undefined) {
49
+ return {
50
+ status: "unavailable",
51
+ reasonCategory: this.reasonCategory,
52
+ points: []
53
+ };
54
+ }
55
+ return {
56
+ status: "available",
57
+ points: this.points.map(clonePoint)
58
+ };
59
+ }
60
+ captureSafely(snapshotFactory, terminal) {
61
+ if (!this.available || (this.terminalCaptured && !terminal)) {
62
+ return false;
63
+ }
64
+ try {
65
+ if (this.startNs === undefined) {
66
+ this.start();
67
+ }
68
+ const nowNs = this.nowNs();
69
+ const elapsedNs = nowNs > this.startNs ? nowNs - this.startNs : 0n;
70
+ const elapsedSeconds = Number(elapsedNs) / 1000000000;
71
+ const scenarios = [...snapshotFactory()]
72
+ .map(projectScenario)
73
+ .sort((left, right) => (left.sortIndex ?? 0) - (right.sortIndex ?? 0)
74
+ || left.scenarioName.localeCompare(right.scenarioName));
75
+ const point = {
76
+ elapsedSeconds: finiteNonNegative(elapsedSeconds) ?? 0,
77
+ terminal,
78
+ scenarios
79
+ };
80
+ if (terminal) {
81
+ this.points = this.points.filter((existing) => existing.elapsedSeconds <= point.elapsedSeconds);
82
+ this.terminalCaptured = true;
83
+ }
84
+ const existingIndex = this.points.findIndex((existing) => existing.elapsedSeconds === point.elapsedSeconds);
85
+ if (existingIndex >= 0) {
86
+ if (terminal || !this.points[existingIndex].terminal) {
87
+ this.points[existingIndex] = point;
88
+ }
89
+ }
90
+ else if (point.elapsedSeconds > 0 || terminal) {
91
+ this.points.push(point);
92
+ this.points.sort((left, right) => left.elapsedSeconds - right.elapsedSeconds);
93
+ }
94
+ this.compact();
95
+ return true;
96
+ }
97
+ catch (error) {
98
+ this.disable("capture_failure");
99
+ try {
100
+ this.onCaptureError?.(error);
101
+ }
102
+ catch {
103
+ // Report diagnostics are best-effort and must never affect the run.
104
+ }
105
+ return false;
106
+ }
107
+ }
108
+ compact() {
109
+ if (this.pointLimit === 2 && this.points.length > 2) {
110
+ this.points = [this.points[0], this.points[this.points.length - 1]];
111
+ return;
112
+ }
113
+ while (this.points.length > this.pointLimit) {
114
+ const newest = this.points[this.points.length - 1];
115
+ const thinned = [this.points[0]];
116
+ for (let index = 1; index < this.points.length - 1; index += 2) {
117
+ thinned.push(this.points[index]);
118
+ }
119
+ if (thinned[thinned.length - 1] !== newest) {
120
+ thinned.push(newest);
121
+ }
122
+ this.points = thinned;
123
+ }
124
+ }
125
+ }
126
+ /**
127
+ * Coordinates one private history worker across every local scenario. The
128
+ * first measured phase establishes the history clock, and the last scenario
129
+ * leaving execution records the terminal point before scenario cleanup.
130
+ */
131
+ export class ReportHistoryLifecycleCoordinator {
132
+ constructor(worker) {
133
+ this.worker = worker;
134
+ this.measuredLoadStarted = false;
135
+ this.endedScenarios = 0;
136
+ this.completed = false;
137
+ this.terminalBoundaryReleased = false;
138
+ this.scenarioCount = Math.max(1, Math.trunc(worker.scenarioCount ?? 1));
139
+ this.terminalBoundary = new Promise((resolve) => {
140
+ this.releaseTerminalBoundary = resolve;
141
+ });
142
+ }
143
+ scenarioBombingStarted() {
144
+ if (this.completed || this.measuredLoadStarted) {
145
+ return;
146
+ }
147
+ this.measuredLoadStarted = true;
148
+ try {
149
+ this.worker.start();
150
+ }
151
+ catch {
152
+ // The private report-only worker cannot fail the measured run.
153
+ }
154
+ }
155
+ async scenarioExecutionEnded() {
156
+ if (this.completed) {
157
+ await this.terminalBoundary;
158
+ return;
159
+ }
160
+ this.endedScenarios += 1;
161
+ if (this.endedScenarios < this.scenarioCount) {
162
+ await this.terminalBoundary;
163
+ return;
164
+ }
165
+ this.completed = true;
166
+ try {
167
+ if (this.measuredLoadStarted) {
168
+ this.worker.stopAndFinalize();
169
+ }
170
+ }
171
+ catch {
172
+ // The private report-only worker cannot fail scenario completion.
173
+ }
174
+ finally {
175
+ this.releaseBoundary();
176
+ }
177
+ await this.terminalBoundary;
178
+ }
179
+ stopWithoutFinalizing() {
180
+ if (this.completed) {
181
+ this.releaseBoundary();
182
+ return;
183
+ }
184
+ this.completed = true;
185
+ try {
186
+ if (this.measuredLoadStarted) {
187
+ this.worker.stopWithoutFinalizing();
188
+ }
189
+ }
190
+ catch {
191
+ // The private report-only worker cannot fail run cleanup.
192
+ }
193
+ finally {
194
+ this.releaseBoundary();
195
+ }
196
+ }
197
+ releaseBoundary() {
198
+ if (!this.terminalBoundaryReleased) {
199
+ this.terminalBoundaryReleased = true;
200
+ this.releaseTerminalBoundary();
201
+ }
202
+ }
203
+ }
204
+ /**
205
+ * Owns the report timer. Every cadence calculation, timeout callback, snapshot,
206
+ * and projection is exception-bounded so report history can never fail a run.
207
+ */
208
+ export class ReportHistoryWorker {
209
+ constructor(options) {
210
+ this.options = options;
211
+ this.cadenceNs = 0n;
212
+ this.nextDeadlineNs = 0n;
213
+ this.running = false;
214
+ this.nowNs = options.nowNs ?? (() => process.hrtime.bigint());
215
+ }
216
+ get started() {
217
+ return this.running;
218
+ }
219
+ start() {
220
+ if (this.running || !this.options.collector.available) {
221
+ return;
222
+ }
223
+ try {
224
+ this.cadenceNs = cadenceNanoseconds(this.options.cadenceSeconds);
225
+ const nowNs = this.nowNs();
226
+ this.options.collector.start(nowNs);
227
+ this.nextDeadlineNs = nowNs + this.cadenceNs;
228
+ this.running = true;
229
+ this.scheduleNext();
230
+ }
231
+ catch (error) {
232
+ this.fail(error);
233
+ }
234
+ }
235
+ stopAndFinalize() {
236
+ if (!this.running) {
237
+ return;
238
+ }
239
+ this.running = false;
240
+ if (this.timer !== undefined) {
241
+ clearTimeout(this.timer);
242
+ this.timer = undefined;
243
+ }
244
+ try {
245
+ this.options.collector.finalize(this.options.snapshotFactory);
246
+ }
247
+ catch (error) {
248
+ this.fail(error);
249
+ }
250
+ }
251
+ stopWithoutFinalizing() {
252
+ this.running = false;
253
+ if (this.timer !== undefined) {
254
+ clearTimeout(this.timer);
255
+ this.timer = undefined;
256
+ }
257
+ }
258
+ scheduleNext() {
259
+ try {
260
+ if (!this.running || !this.options.collector.available) {
261
+ return;
262
+ }
263
+ const remainingNs = this.nextDeadlineNs - this.nowNs();
264
+ const delayMs = safeTimeoutDelayMs(remainingNs);
265
+ this.timer = setTimeout(() => this.onTimer(), delayMs);
266
+ if (typeof this.timer.unref === "function") {
267
+ this.timer.unref();
268
+ }
269
+ }
270
+ catch (error) {
271
+ this.fail(error);
272
+ }
273
+ }
274
+ onTimer() {
275
+ try {
276
+ this.timer = undefined;
277
+ if (!this.running || !this.options.collector.available) {
278
+ return;
279
+ }
280
+ const nowNs = this.nowNs();
281
+ if (nowNs < this.nextDeadlineNs) {
282
+ this.scheduleNext();
283
+ return;
284
+ }
285
+ this.options.collector.capture(this.options.snapshotFactory);
286
+ if (!this.options.collector.available) {
287
+ this.running = false;
288
+ return;
289
+ }
290
+ do {
291
+ this.nextDeadlineNs += this.cadenceNs;
292
+ } while (this.nextDeadlineNs <= nowNs);
293
+ this.scheduleNext();
294
+ }
295
+ catch (error) {
296
+ this.fail(error);
297
+ }
298
+ }
299
+ fail(error) {
300
+ this.stopWithoutFinalizing();
301
+ this.options.collector.disable("capture_failure");
302
+ try {
303
+ this.options.onFailure?.("capture_failure", sanitizedExceptionClassChain(error));
304
+ }
305
+ catch {
306
+ // Logging a report-only warning is best-effort.
307
+ }
308
+ }
309
+ }
310
+ export function sanitizedExceptionClassChain(error) {
311
+ const classes = [];
312
+ const seen = new Set();
313
+ let current = error;
314
+ while (current !== undefined && current !== null && classes.length < 8 && !seen.has(current)) {
315
+ seen.add(current);
316
+ const rawName = typeof current === "object"
317
+ ? String(current.constructor?.name ?? "Error")
318
+ : typeof current;
319
+ const safeName = rawName.replace(/[^A-Za-z0-9_.$-]/g, "_").slice(0, 64) || "Error";
320
+ classes.push(safeName);
321
+ current = typeof current === "object"
322
+ ? current.cause
323
+ : undefined;
324
+ }
325
+ return classes.join(" -> ") || "Error";
326
+ }
327
+ function cadenceNanoseconds(seconds) {
328
+ if (!Number.isFinite(seconds) || seconds <= 0) {
329
+ throw new RangeError("Invalid report history cadence.");
330
+ }
331
+ const safelyRepresentableSeconds = Math.min(seconds, Number.MAX_SAFE_INTEGER);
332
+ const wholeSeconds = Math.trunc(safelyRepresentableSeconds);
333
+ const fractionalNanoseconds = Math.round(Math.max(0, safelyRepresentableSeconds - wholeSeconds) * 1000000000);
334
+ const cadence = BigInt(wholeSeconds) * 1000000000n + BigInt(fractionalNanoseconds);
335
+ if (cadence <= 0n) {
336
+ throw new RangeError("Report history cadence is below one nanosecond.");
337
+ }
338
+ return cadence;
339
+ }
340
+ function safeTimeoutDelayMs(remainingNs) {
341
+ if (remainingNs <= 0n) {
342
+ return 1;
343
+ }
344
+ const milliseconds = (remainingNs + 999999n) / 1000000n;
345
+ return Number(milliseconds > 2147483647n ? 2147483647n : milliseconds);
346
+ }
347
+ export function deriveScenarioRates(points) {
348
+ const scenarioNames = [...new Set(points.flatMap((point) => point.scenarios.map((scenario) => scenario.scenarioName)))];
349
+ return scenarioNames.map((scenarioName) => {
350
+ let previousElapsed = 0;
351
+ let previousCount = 0;
352
+ return {
353
+ scenarioName,
354
+ values: points.map((point) => {
355
+ const scenario = point.scenarios.find((candidate) => candidate.scenarioName === scenarioName);
356
+ const currentCount = scenario?.all?.count
357
+ ?? ((scenario?.ok?.count ?? 0) + (scenario?.failed?.count ?? 0));
358
+ const duration = point.elapsedSeconds - previousElapsed;
359
+ const rate = duration > 0
360
+ ? Math.max(0, currentCount - previousCount) / duration
361
+ : null;
362
+ previousElapsed = point.elapsedSeconds;
363
+ previousCount = currentCount;
364
+ return rate === null || !Number.isFinite(rate) ? null : rate;
365
+ })
366
+ };
367
+ });
368
+ }
369
+ function projectScenario(source) {
370
+ return {
371
+ scenarioName: String(source.scenarioName ?? ""),
372
+ sortIndex: finiteNonNegativeInteger(source.sortIndex),
373
+ ...(source.all ? { all: projectMeasurement(source.all) } : {}),
374
+ ...(source.ok ? { ok: projectMeasurement(source.ok) } : {}),
375
+ ...(source.failed ? { failed: projectMeasurement(source.failed) } : {})
376
+ };
377
+ }
378
+ function projectMeasurement(source) {
379
+ return {
380
+ count: finiteNonNegativeInteger(source.count) ?? 0,
381
+ bytes: finiteNonNegative(source.bytes) ?? 0,
382
+ approximate: source.approximate === true,
383
+ ...optionalFinite("percent50Ms", source.percent50Ms),
384
+ ...optionalFinite("percent75Ms", source.percent75Ms),
385
+ ...optionalFinite("percent95Ms", source.percent95Ms),
386
+ ...optionalFinite("percent99Ms", source.percent99Ms)
387
+ };
388
+ }
389
+ function optionalFinite(key, value) {
390
+ const normalized = finiteNonNegative(value);
391
+ return normalized === undefined ? {} : { [key]: normalized };
392
+ }
393
+ function finiteNonNegative(value) {
394
+ return typeof value === "number" && Number.isFinite(value) && value >= 0
395
+ ? value
396
+ : undefined;
397
+ }
398
+ function finiteNonNegativeInteger(value) {
399
+ const normalized = finiteNonNegative(value);
400
+ return normalized === undefined ? undefined : Math.trunc(normalized);
401
+ }
402
+ function clonePoint(point) {
403
+ return {
404
+ elapsedSeconds: point.elapsedSeconds,
405
+ terminal: point.terminal,
406
+ scenarios: point.scenarios.map((scenario) => ({
407
+ ...scenario,
408
+ ...(scenario.all ? { all: { ...scenario.all } } : {}),
409
+ ...(scenario.ok ? { ok: { ...scenario.ok } } : {}),
410
+ ...(scenario.failed ? { failed: { ...scenario.failed } } : {})
411
+ }))
412
+ };
413
+ }