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