@loadstrike/loadstrike-sdk 1.0.31001 → 1.0.31601
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 +2 -0
- package/dist/cjs/local-report-input.js +21 -0
- package/dist/cjs/report-history.js +421 -0
- package/dist/cjs/reporting-svg.js +116 -0
- package/dist/cjs/reporting.js +413 -136
- package/dist/cjs/runtime.js +154 -5
- package/dist/esm/local-report-input.js +17 -0
- package/dist/esm/report-history.js +413 -0
- package/dist/esm/reporting-svg.js +113 -0
- package/dist/esm/reporting.js +413 -136
- package/dist/esm/runtime.js +154 -5
- package/dist/types/local-report-input.d.ts +6 -0
- package/dist/types/report-history.d.ts +124 -0
- package/dist/types/reporting-svg.d.ts +2 -0
- package/dist/types/reporting.d.ts +6 -3
- package/dist/types/runtime.d.ts +23 -0
- package/package.json +1 -1
package/dist/cjs/runtime.js
CHANGED
|
@@ -13,6 +13,8 @@ const cluster_js_1 = require("./cluster.js");
|
|
|
13
13
|
const correlation_js_1 = require("./correlation.js");
|
|
14
14
|
const transports_js_1 = require("./transports.js");
|
|
15
15
|
const reporting_js_1 = require("./reporting.js");
|
|
16
|
+
const report_history_js_1 = require("./report-history.js");
|
|
17
|
+
const local_report_input_js_1 = require("./local-report-input.js");
|
|
16
18
|
const sinks_js_1 = require("./sinks.js");
|
|
17
19
|
const load_engine_v2_js_1 = require("./load-engine-v2.js");
|
|
18
20
|
const iteration_observations_js_1 = require("./iteration-observations.js");
|
|
@@ -291,6 +293,28 @@ class MeasurementAccumulator {
|
|
|
291
293
|
moreOrEq1200: left.moreOrEq1200 + right.moreOrEq1200
|
|
292
294
|
}, allRequestCount, durationMs);
|
|
293
295
|
}
|
|
296
|
+
/**
|
|
297
|
+
* Projects only the cumulative fields used by the private HTML-report
|
|
298
|
+
* history. Legacy distributions deliberately omit latency so this cadence
|
|
299
|
+
* path never scans their retained per-request arrays.
|
|
300
|
+
*/
|
|
301
|
+
buildReportHistoryMeasurement() {
|
|
302
|
+
return projectNativeReportHistoryMeasurement(this.count, this.allBytes, this.useHistogram ? this.latencyHistogram : undefined);
|
|
303
|
+
}
|
|
304
|
+
/**
|
|
305
|
+
* Combines only bounded native latency state plus exact counters. It does
|
|
306
|
+
* not materialize status rows or any public measurement DTO.
|
|
307
|
+
*/
|
|
308
|
+
buildCombinedReportHistoryMeasurement(other) {
|
|
309
|
+
const count = this.count + other.count;
|
|
310
|
+
const bytes = this.allBytes + other.allBytes;
|
|
311
|
+
if (!this.useHistogram || !other.useHistogram) {
|
|
312
|
+
return projectNativeReportHistoryMeasurement(count, bytes);
|
|
313
|
+
}
|
|
314
|
+
const latency = this.latencyHistogram.clone();
|
|
315
|
+
latency.merge(other.latencyHistogram);
|
|
316
|
+
return projectNativeReportHistoryMeasurement(count, bytes, latency);
|
|
317
|
+
}
|
|
294
318
|
histogramSnapshot() {
|
|
295
319
|
return {
|
|
296
320
|
count: this.count,
|
|
@@ -304,6 +328,24 @@ class MeasurementAccumulator {
|
|
|
304
328
|
};
|
|
305
329
|
}
|
|
306
330
|
}
|
|
331
|
+
function projectNativeReportHistoryMeasurement(count, bytes, latency) {
|
|
332
|
+
const measurement = {
|
|
333
|
+
count,
|
|
334
|
+
bytes,
|
|
335
|
+
approximate: latency?.maxRelativeError !== undefined
|
|
336
|
+
&& latency.maxRelativeError > 0
|
|
337
|
+
};
|
|
338
|
+
if (!latency || latency.count === 0n) {
|
|
339
|
+
return measurement;
|
|
340
|
+
}
|
|
341
|
+
return {
|
|
342
|
+
...measurement,
|
|
343
|
+
percent50Ms: Number(latency.percentile(0.5)) / 1000,
|
|
344
|
+
percent75Ms: Number(latency.percentile(0.75)) / 1000,
|
|
345
|
+
percent95Ms: Number(latency.percentile(0.95)) / 1000,
|
|
346
|
+
percent99Ms: Number(latency.percentile(0.99)) / 1000
|
|
347
|
+
};
|
|
348
|
+
}
|
|
307
349
|
function buildHistogramMeasurement(snapshot, allRequestCount, durationMs) {
|
|
308
350
|
const count = snapshot.count;
|
|
309
351
|
const totalDurationMs = Math.max(durationMs, 0);
|
|
@@ -642,6 +684,21 @@ class ScenarioStatsAccumulator {
|
|
|
642
684
|
step.record(reply, observedLatencyMs);
|
|
643
685
|
return step.sortIndex;
|
|
644
686
|
}
|
|
687
|
+
/**
|
|
688
|
+
* Captures the bounded, private history view without building scenario
|
|
689
|
+
* steps, status rows, plugins, aliases, or any other public result shape.
|
|
690
|
+
*/
|
|
691
|
+
buildReportHistorySnapshot() {
|
|
692
|
+
const ok = this.ok.buildReportHistoryMeasurement();
|
|
693
|
+
const failed = this.fail.buildReportHistoryMeasurement();
|
|
694
|
+
return {
|
|
695
|
+
scenarioName: this.scenarioName,
|
|
696
|
+
sortIndex: this.sortIndex,
|
|
697
|
+
all: this.ok.buildCombinedReportHistoryMeasurement(this.fail),
|
|
698
|
+
...(ok.count > 0 ? { ok } : {}),
|
|
699
|
+
...(failed.count > 0 ? { failed } : {})
|
|
700
|
+
};
|
|
701
|
+
}
|
|
645
702
|
/**
|
|
646
703
|
* Builds the configured payload or helper object.
|
|
647
704
|
* Use this when all builder inputs are ready to be materialized.
|
|
@@ -3509,6 +3566,58 @@ class LoadStrikeRunner {
|
|
|
3509
3566
|
licenseSession = await licenseClient.acquireLicenseLease(licensePayload);
|
|
3510
3567
|
const loggerSetup = createLoggerSetup(this.options.loggerConfig, this.options.minimumLogLevel, this.options, testInfo, nodeInfo);
|
|
3511
3568
|
const runLogger = loggerSetup.logger;
|
|
3569
|
+
const htmlReportHistoryRequested = (this.options.reportsEnabled ?? true)
|
|
3570
|
+
&& normalizeReportFormats(this.options.reportFormats ?? ["html", "txt", "csv", "md"]).includes("html");
|
|
3571
|
+
const distributedReportHistory = isDistributedReportHistoryExecution(clusterMode, this.options);
|
|
3572
|
+
let localReportInput = htmlReportHistoryRequested
|
|
3573
|
+
&& distributedReportHistory
|
|
3574
|
+
? (0, local_report_input_js_1.distributedLocalReportInput)()
|
|
3575
|
+
: (0, local_report_input_js_1.emptyLocalReportInput)();
|
|
3576
|
+
let reportHistoryCollector;
|
|
3577
|
+
let reportHistoryLifecycle;
|
|
3578
|
+
if (htmlReportHistoryRequested && !distributedReportHistory) {
|
|
3579
|
+
const warnReportHistoryFailure = (category, error) => {
|
|
3580
|
+
const exceptionClasses = error === undefined
|
|
3581
|
+
? ""
|
|
3582
|
+
: `; exception_classes=${(0, report_history_js_1.sanitizedExceptionClassChain)(error)}`;
|
|
3583
|
+
try {
|
|
3584
|
+
runLogger.warn(`LoadStrike report history unavailable: ${category}${exceptionClasses}.`);
|
|
3585
|
+
}
|
|
3586
|
+
catch {
|
|
3587
|
+
// Report-only diagnostics are best-effort.
|
|
3588
|
+
}
|
|
3589
|
+
};
|
|
3590
|
+
reportHistoryCollector = new report_history_js_1.ReportHistoryCollector({
|
|
3591
|
+
scenarioCount: selectedScenarios.length,
|
|
3592
|
+
onCaptureError: (error) => warnReportHistoryFailure("capture_failure", error)
|
|
3593
|
+
});
|
|
3594
|
+
if (!reportHistoryCollector.available) {
|
|
3595
|
+
warnReportHistoryFailure("budget_pressure");
|
|
3596
|
+
}
|
|
3597
|
+
const snapshotFactory = () => Array.from(scenarioAccumulators.values())
|
|
3598
|
+
.map((value) => value.buildReportHistorySnapshot())
|
|
3599
|
+
.sort((left, right) => (left.sortIndex ?? 0) - (right.sortIndex ?? 0)
|
|
3600
|
+
|| left.scenarioName.localeCompare(right.scenarioName));
|
|
3601
|
+
const reportHistoryWorker = new report_history_js_1.ReportHistoryWorker({
|
|
3602
|
+
collector: reportHistoryCollector,
|
|
3603
|
+
cadenceSeconds: resolvedReportHistoryCadenceSeconds(this.options.reportingIntervalSeconds ?? 5),
|
|
3604
|
+
snapshotFactory,
|
|
3605
|
+
onFailure: (category, exceptionClasses) => {
|
|
3606
|
+
try {
|
|
3607
|
+
runLogger.warn(`LoadStrike report history unavailable: ${category}; exception_classes=${exceptionClasses}.`);
|
|
3608
|
+
}
|
|
3609
|
+
catch {
|
|
3610
|
+
// Report-only diagnostics are best-effort.
|
|
3611
|
+
}
|
|
3612
|
+
}
|
|
3613
|
+
});
|
|
3614
|
+
reportHistoryLifecycle = new report_history_js_1.ReportHistoryLifecycleCoordinator({
|
|
3615
|
+
scenarioCount: selectedScenarios.length,
|
|
3616
|
+
start: () => reportHistoryWorker.start(),
|
|
3617
|
+
stopAndFinalize: () => reportHistoryWorker.stopAndFinalize(),
|
|
3618
|
+
stopWithoutFinalizing: () => reportHistoryWorker.stopWithoutFinalizing()
|
|
3619
|
+
});
|
|
3620
|
+
}
|
|
3512
3621
|
const scenarioStartInfos = selectedScenarios.map((scenario, index) => {
|
|
3513
3622
|
const startInfo = {
|
|
3514
3623
|
scenarioName: scenario.name,
|
|
@@ -3618,7 +3727,7 @@ class LoadStrikeRunner {
|
|
|
3618
3727
|
realtimeInFlight = false;
|
|
3619
3728
|
}
|
|
3620
3729
|
};
|
|
3621
|
-
const reportingIntervalMs =
|
|
3730
|
+
const reportingIntervalMs = boundedReportingIntervalMs(this.options.reportingIntervalSeconds ?? 5);
|
|
3622
3731
|
const triggerRealtimeSnapshot = () => {
|
|
3623
3732
|
if (realtimeCurrent) {
|
|
3624
3733
|
return;
|
|
@@ -3705,6 +3814,7 @@ class LoadStrikeRunner {
|
|
|
3705
3814
|
iterationObservationRunId,
|
|
3706
3815
|
iterationObservationResultOwnerId,
|
|
3707
3816
|
iterationObservationProcessGroup,
|
|
3817
|
+
reportHistoryLifecycle,
|
|
3708
3818
|
executeScenarioInvocation: (targetScenario, context, operation) => this.executeScenarioInvocation(targetScenario, context, operation),
|
|
3709
3819
|
invokeBeforeScenario: (runtimePolicies, scenarioName) => this.invokeBeforeScenario(runtimePolicies, scenarioName, policyErrors, runtimePolicyErrorMode),
|
|
3710
3820
|
invokeAfterScenario: (runtimePolicies, scenarioName, stats) => this.invokeAfterScenario(runtimePolicies, scenarioName, stats, policyErrors, runtimePolicyErrorMode),
|
|
@@ -3792,9 +3902,14 @@ class LoadStrikeRunner {
|
|
|
3792
3902
|
...iterationObservationReporter.buildWarnings()
|
|
3793
3903
|
];
|
|
3794
3904
|
result.pluginsData = mergePluginData(result.pluginsData, await this.collectPluginData(plugins, attachRunResultAliases(result), pluginLifecycleErrors));
|
|
3905
|
+
if (reportHistoryCollector) {
|
|
3906
|
+
localReportInput = {
|
|
3907
|
+
history: reportHistoryCollector.toProjection()
|
|
3908
|
+
};
|
|
3909
|
+
}
|
|
3795
3910
|
const finalizedResult = attachRunResultAliases(result);
|
|
3796
3911
|
finalizedResult.logFiles = mergeStringArrays(finalizedResult.logFiles, loggerSetup.logFiles);
|
|
3797
|
-
finalizedResult.reportFiles = this.writeReports(finalizedResult);
|
|
3912
|
+
finalizedResult.reportFiles = this.writeReports(finalizedResult, localReportInput);
|
|
3798
3913
|
finalizedResult.disabledSinks = sinkStates.filter((x) => x.disabled).map((x) => x.name);
|
|
3799
3914
|
await this.emitRunResult(sinkStates, finalizedResult, sinkRetryCount, sinkRetryBackoffMs, sinkErrors, testInfo.sessionId, runLogger);
|
|
3800
3915
|
await this.stopSinks(sinkStates, sinkRetryCount, sinkRetryBackoffMs, sinkErrors, testInfo.sessionId, runLogger);
|
|
@@ -3805,6 +3920,7 @@ class LoadStrikeRunner {
|
|
|
3805
3920
|
return finalizedResult;
|
|
3806
3921
|
}
|
|
3807
3922
|
finally {
|
|
3923
|
+
reportHistoryLifecycle?.stopWithoutFinalizing();
|
|
3808
3924
|
await stopRealtimeReporting();
|
|
3809
3925
|
if (!iterationObservationsFinalized) {
|
|
3810
3926
|
await iterationObservationReporter.sealAndDrain().catch(() => { });
|
|
@@ -4282,7 +4398,7 @@ class LoadStrikeRunner {
|
|
|
4282
4398
|
message: String(error ?? "runtime policy callback failed")
|
|
4283
4399
|
});
|
|
4284
4400
|
}
|
|
4285
|
-
writeReports(result) {
|
|
4401
|
+
writeReports(result, localReportInput = (0, local_report_input_js_1.emptyLocalReportInput)()) {
|
|
4286
4402
|
const reportsEnabled = this.options.reportsEnabled ?? true;
|
|
4287
4403
|
if (!reportsEnabled) {
|
|
4288
4404
|
return [];
|
|
@@ -4307,7 +4423,7 @@ class LoadStrikeRunner {
|
|
|
4307
4423
|
(0, node_fs_1.writeFileSync)(path, (0, reporting_js_1.buildDotnetMarkdownReport)(nodeStats), "utf8");
|
|
4308
4424
|
}
|
|
4309
4425
|
else if (format === "html") {
|
|
4310
|
-
(0, node_fs_1.writeFileSync)(path, (0, reporting_js_1.buildDotnetHtmlReport)(nodeStats), "utf8");
|
|
4426
|
+
(0, node_fs_1.writeFileSync)(path, (0, reporting_js_1.buildDotnetHtmlReport)(nodeStats, localReportInput), "utf8");
|
|
4311
4427
|
}
|
|
4312
4428
|
written.push(path);
|
|
4313
4429
|
}
|
|
@@ -4341,6 +4457,19 @@ function hasPluginRows(value) {
|
|
|
4341
4457
|
}
|
|
4342
4458
|
return value.tables.some((table) => Array.isArray(table.rows) && table.rows.length > 0);
|
|
4343
4459
|
}
|
|
4460
|
+
function boundedReportingIntervalMs(seconds) {
|
|
4461
|
+
if (!Number.isFinite(seconds) || seconds <= 0) {
|
|
4462
|
+
return 1;
|
|
4463
|
+
}
|
|
4464
|
+
const milliseconds = seconds * 1000;
|
|
4465
|
+
if (!Number.isFinite(milliseconds) || milliseconds >= 2147483647) {
|
|
4466
|
+
return 2147483647;
|
|
4467
|
+
}
|
|
4468
|
+
return Math.max(Math.trunc(milliseconds), 1);
|
|
4469
|
+
}
|
|
4470
|
+
function resolvedReportHistoryCadenceSeconds(seconds) {
|
|
4471
|
+
return boundedReportingIntervalMs(seconds) / 1000;
|
|
4472
|
+
}
|
|
4344
4473
|
async function executeV2FixedArrivals(args) {
|
|
4345
4474
|
const { rate, intervalNs, totalArrivals, budget, cancellationToken, shouldStopNow, nextInstanceInfo, runBombingInvocation, logger, scenarioName, deadlineOffsetsNs, tolerancesNs, ownedOrdinals, shardIndex = 0, shardCount = 1, telemetry, segment } = args;
|
|
4346
4475
|
const segmentStartNs = process.hrtime.bigint();
|
|
@@ -4477,7 +4606,15 @@ function maxBigInt(left, right) {
|
|
|
4477
4606
|
return left > right ? left : right;
|
|
4478
4607
|
}
|
|
4479
4608
|
async function executeScenarioRuntime(args) {
|
|
4480
|
-
const { scenario, scenarioIndex, scenarioCount, options, logger, nodeInfo, testInfo, policies, restartIterationMaxAttempts, allRegisteredMetrics, scenarioRuntimes, stepRuntimes, scenarioAccumulators, scenarioDurationsMs, stopTestState, testAbortController, loadEngineV2Budget, loadEngineV2Telemetry, iterationObservationReporter, iterationObservationRunId = testInfo.sessionId, iterationObservationResultOwnerId = "", iterationObservationProcessGroup = 0, executeScenarioInvocation, invokeBeforeScenario, invokeAfterScenario, invokeBeforeStep, invokeAfterStep } = args;
|
|
4609
|
+
const { scenario, scenarioIndex, scenarioCount, options, logger, nodeInfo, testInfo, policies, restartIterationMaxAttempts, allRegisteredMetrics, scenarioRuntimes, stepRuntimes, scenarioAccumulators, scenarioDurationsMs, stopTestState, testAbortController, loadEngineV2Budget, loadEngineV2Telemetry, iterationObservationReporter, iterationObservationRunId = testInfo.sessionId, iterationObservationResultOwnerId = "", iterationObservationProcessGroup = 0, reportHistoryLifecycle, executeScenarioInvocation, invokeBeforeScenario, invokeAfterScenario, invokeBeforeStep, invokeAfterStep } = args;
|
|
4610
|
+
let reportHistoryExecutionEnded = false;
|
|
4611
|
+
const endReportHistoryExecution = async () => {
|
|
4612
|
+
if (reportHistoryExecutionEnded) {
|
|
4613
|
+
return;
|
|
4614
|
+
}
|
|
4615
|
+
reportHistoryExecutionEnded = true;
|
|
4616
|
+
await reportHistoryLifecycle?.scenarioExecutionEnded();
|
|
4617
|
+
};
|
|
4481
4618
|
const scenarioStartedMs = Date.now();
|
|
4482
4619
|
const scenarioContextData = {};
|
|
4483
4620
|
const registeredMetrics = [];
|
|
@@ -5298,6 +5435,7 @@ async function executeScenarioRuntime(args) {
|
|
|
5298
5435
|
})));
|
|
5299
5436
|
await invokeBeforeScenario(policies, scenario.name);
|
|
5300
5437
|
await runWarmUpAsync();
|
|
5438
|
+
reportHistoryLifecycle?.scenarioBombingStarted();
|
|
5301
5439
|
accumulator.setCurrentOperation("Bombing");
|
|
5302
5440
|
const simulations = scenario.getSimulations();
|
|
5303
5441
|
if (!simulations.length) {
|
|
@@ -5331,6 +5469,7 @@ async function executeScenarioRuntime(args) {
|
|
|
5331
5469
|
}
|
|
5332
5470
|
}
|
|
5333
5471
|
accumulator.setCurrentOperation(stopScenario || stopTestState.value ? "Stop" : "Complete");
|
|
5472
|
+
await endReportHistoryExecution();
|
|
5334
5473
|
await invokeAfterScenario(policies, scenario.name, runtime);
|
|
5335
5474
|
}
|
|
5336
5475
|
catch (error) {
|
|
@@ -5347,6 +5486,7 @@ async function executeScenarioRuntime(args) {
|
|
|
5347
5486
|
}
|
|
5348
5487
|
}
|
|
5349
5488
|
finally {
|
|
5489
|
+
await endReportHistoryExecution();
|
|
5350
5490
|
scenarioDurationsMs.set(scenario.name, Math.max(Date.now() - scenarioStartedMs, 0));
|
|
5351
5491
|
try {
|
|
5352
5492
|
await scenario.invokeClean({
|
|
@@ -7280,6 +7420,11 @@ function resolveClusterExecutionMode(options) {
|
|
|
7280
7420
|
}
|
|
7281
7421
|
return "single";
|
|
7282
7422
|
}
|
|
7423
|
+
function isDistributedReportHistoryExecution(clusterMode, options) {
|
|
7424
|
+
return clusterMode !== "single"
|
|
7425
|
+
|| options.loadEngineV2SegmentLifecycleOverride !== undefined
|
|
7426
|
+
|| Math.max(Math.trunc(options.clusterShardCount ?? 1), 1) > 1;
|
|
7427
|
+
}
|
|
7283
7428
|
function buildEmptyNodeStats(args) {
|
|
7284
7429
|
return attachNodeStatsAliases({
|
|
7285
7430
|
startedUtc: args.startedUtc,
|
|
@@ -11111,6 +11256,7 @@ function buildGroupedCorrelationRows(rows) {
|
|
|
11111
11256
|
LatencyMinMs: latencySamples.length ? formatOptionalLatency(latencySamples[0]) : "",
|
|
11112
11257
|
LatencyMeanMs: latencySamples.length ? formatOptionalLatency(latencySamples.reduce((sum, value) => sum + value, 0) / latencySamples.length) : "",
|
|
11113
11258
|
LatencyP50Ms: latencySamples.length ? formatOptionalLatency(percentile(latencySamples, 0.5)) : "",
|
|
11259
|
+
LatencyP75Ms: latencySamples.length ? formatOptionalLatency(percentile(latencySamples, 0.75)) : "",
|
|
11114
11260
|
LatencyP80Ms: latencySamples.length ? formatOptionalLatency(percentile(latencySamples, 0.8)) : "",
|
|
11115
11261
|
LatencyP85Ms: latencySamples.length ? formatOptionalLatency(percentile(latencySamples, 0.85)) : "",
|
|
11116
11262
|
LatencyP90Ms: latencySamples.length ? formatOptionalLatency(percentile(latencySamples, 0.9)) : "",
|
|
@@ -11323,6 +11469,8 @@ exports.__loadstrikeTestExports = {
|
|
|
11323
11469
|
buildThresholdCheckExpression,
|
|
11324
11470
|
buildTrackingLeaseKey,
|
|
11325
11471
|
buildTrackingRunNamespace,
|
|
11472
|
+
boundedReportingIntervalMs,
|
|
11473
|
+
resolvedReportHistoryCadenceSeconds,
|
|
11326
11474
|
clusterNodeResultToNodeStats,
|
|
11327
11475
|
combineAbortSignals,
|
|
11328
11476
|
computeScenarioRequestCount,
|
|
@@ -11342,6 +11490,7 @@ exports.__loadstrikeTestExports = {
|
|
|
11342
11490
|
formatUtcReportTimestamp,
|
|
11343
11491
|
hasPluginRows,
|
|
11344
11492
|
inferRuntimeLegacyHttpResponseSource,
|
|
11493
|
+
isDistributedReportHistoryExecution,
|
|
11345
11494
|
isComparisonFailed,
|
|
11346
11495
|
loadJsonObject,
|
|
11347
11496
|
logLevelOrder,
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
export function emptyLocalReportInput() {
|
|
2
|
+
return {
|
|
3
|
+
history: {
|
|
4
|
+
status: "available",
|
|
5
|
+
points: []
|
|
6
|
+
}
|
|
7
|
+
};
|
|
8
|
+
}
|
|
9
|
+
export function distributedLocalReportInput() {
|
|
10
|
+
return {
|
|
11
|
+
history: {
|
|
12
|
+
status: "unavailable",
|
|
13
|
+
reasonCategory: "distributed_temporal_aggregation_unavailable",
|
|
14
|
+
points: []
|
|
15
|
+
}
|
|
16
|
+
};
|
|
17
|
+
}
|