@loadstrike/loadstrike-sdk 1.0.22601 → 1.0.23001

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.
@@ -12,11 +12,13 @@ var __classPrivateFieldGet = (this && this.__classPrivateFieldGet) || function (
12
12
  var _LoadStrikeAutopilotResult_httpRequest;
13
13
  import fs from "node:fs";
14
14
  import { LoadStrikeResponse, LoadStrikeScenario, LoadStrikeSimulation, LoadStrikeThreshold } from "./runtime.js";
15
+ import { LoadStrikeLocalClient } from "./local.js";
15
16
  import { LoadStrikeAutopilotReadiness } from "./autopilot-contracts.js";
16
17
  const REDACTED = "[REDACTED]";
17
18
  const ENV_MARKER_PREFIX = "${LOADSTRIKE_ENV:";
18
19
  const ENV_MARKER_SUFFIX = "}";
19
20
  const TRACE_TO_TEST_AUTOPILOT_FEATURE = "autopilot.trace_to_test";
21
+ let autopilotLicenseValidationBypassForTests = false;
20
22
  const SECRET_KEYS = new Set([
21
23
  "authorization",
22
24
  "proxy_authorization",
@@ -88,9 +90,10 @@ export class LoadStrikeAutopilotResult {
88
90
  }
89
91
  _LoadStrikeAutopilotResult_httpRequest = new WeakMap();
90
92
  export class LoadStrikeAutopilot {
91
- static generate(request) {
93
+ static async generate(request) {
92
94
  const artifact = loadAutopilotArtifact(request);
93
95
  const options = request.Options ?? {};
96
+ await validateGenerationEntitlement(options);
94
97
  const result = inferAutopilotResult(artifact, options);
95
98
  if (options.IncludePreviewReport) {
96
99
  result.PreviewReport = buildPreviewReport(result);
@@ -101,6 +104,57 @@ export class LoadStrikeAutopilot {
101
104
  return LoadStrikeAutopilot.generate(request);
102
105
  }
103
106
  }
107
+ export const __private = {
108
+ setAutopilotLicenseValidationBypassForTests(value) {
109
+ autopilotLicenseValidationBypassForTests = value;
110
+ }
111
+ };
112
+ async function validateGenerationEntitlement(options) {
113
+ if (autopilotLicenseValidationBypassForTests) {
114
+ return;
115
+ }
116
+ const licensePayload = {
117
+ Context: {
118
+ RunnerKey: options.RunnerKey ?? "",
119
+ LicenseValidationTimeoutSeconds: options.LicenseValidationTimeoutSeconds,
120
+ TestSuite: "loadstrike-autopilot",
121
+ TestName: options.ScenarioName?.trim() || "autopilot-generation"
122
+ },
123
+ Scenarios: [
124
+ {
125
+ Name: "autopilot-generation",
126
+ InternalLicenseFeatures: [TRACE_TO_TEST_AUTOPILOT_FEATURE]
127
+ }
128
+ ],
129
+ RunArgs: []
130
+ };
131
+ const client = new LoadStrikeLocalClient({
132
+ licenseValidationTimeoutMs: normalizeLicenseValidationTimeoutMs(options.LicenseValidationTimeoutSeconds)
133
+ });
134
+ let session;
135
+ try {
136
+ session = await client.acquireLicenseLease(licensePayload);
137
+ }
138
+ catch (error) {
139
+ const message = error instanceof Error ? error.message : String(error);
140
+ if (message.toLowerCase().includes("runner key is required")) {
141
+ throw new Error("Runner key is required for Trace-To-Test Autopilot generation. " +
142
+ "Set Options.RunnerKey before calling LoadStrikeAutopilot.generate(...).");
143
+ }
144
+ throw error;
145
+ }
146
+ finally {
147
+ if (session) {
148
+ await client.releaseLicenseLease(session, licensePayload);
149
+ }
150
+ }
151
+ }
152
+ function normalizeLicenseValidationTimeoutMs(value) {
153
+ if (typeof value !== "number" || !Number.isFinite(value) || value <= 0) {
154
+ return undefined;
155
+ }
156
+ return Math.trunc(value * 1000);
157
+ }
104
158
  function loadAutopilotArtifact(request) {
105
159
  if (!request || typeof request.Kind !== "string" || !request.Kind.trim()) {
106
160
  throw new Error("Autopilot kind must be provided.");
package/dist/esm/index.js CHANGED
@@ -1,6 +1,6 @@
1
1
  export { LoadStrikeAutopilot, LoadStrikeAutopilotResult } from "./autopilot.js";
2
2
  export { LoadStrikeAutopilotReadiness } from "./autopilot-contracts.js";
3
- export { CrossPlatformScenarioConfigurator, ScenarioTrackingExtensions, LoadStrikeContext, LoadStrikePluginData, LoadStrikePluginDataTable, LoadStrikeNodeType, LoadStrikeReportFormat, LoadStrikeResponse, LoadStrikeLogLevel, LoadStrikeScenarioOperation, LoadStrikeOperationType, LoadStrikeRunner, LoadStrikeScenario, LoadStrikeSimulation, LoadStrikeMetric, LoadStrikeCounter, LoadStrikeGauge, LoadStrikeStep, LoadStrikeThreshold } from "./runtime.js";
3
+ export { CrossPlatformScenarioConfigurator, ScenarioTrackingExtensions, LoadStrikeContext, LoadStrikePluginData, LoadStrikePluginDataTable, LoadStrikeNodeType, LoadStrikeReportFormat, LoadStrikeResponse, LoadStrikeLogLevel, LoadStrikeScenarioOperation, LoadStrikeOperationType, LoadStrikeRunner, LoadStrikeScenario, LoadStrikeSimulation, CrossPlatformTrackingConfiguration, LoadStrikeMetric, LoadStrikeCounter, LoadStrikeGauge, LoadStrikeStep, LoadStrikeThreshold } from "./runtime.js";
4
4
  export { CorrelationStoreConfiguration, CrossPlatformTrackingRuntime, InMemoryCorrelationStore, RedisCorrelationStoreOptions, RedisCorrelationStore, TrackingPayloadBuilder, TrackingFieldSelector } from "./correlation.js";
5
- export { EndpointAdapterFactory, LOADSTRIKE_TRACE_ID_HEADER, LOADSTRIKE_TRACE_ID_TRACKING_FIELD, TrafficEndpointDefinition, HttpEndpointDefinition, KafkaEndpointDefinition, KafkaSaslOptions, RabbitMqEndpointDefinition, NatsEndpointDefinition, RedisStreamsEndpointDefinition, AzureEventHubsEndpointDefinition, DelegateStreamEndpointDefinition, PushDiffusionEndpointDefinition, HttpOAuth2ClientCredentialsOptions, HttpAuthOptions } from "./transports.js";
5
+ export { LOADSTRIKE_TRACE_ID_HEADER, LOADSTRIKE_TRACE_ID_TRACKING_FIELD, TrafficEndpointDefinition, HttpEndpointDefinition, KafkaEndpointDefinition, KafkaSaslOptions, RabbitMqEndpointDefinition, NatsEndpointDefinition, RedisStreamsEndpointDefinition, AzureEventHubsEndpointDefinition, SqsEndpointDefinition, DelegateStreamEndpointDefinition, PushDiffusionEndpointDefinition, HttpOAuth2ClientCredentialsOptions, HttpAuthOptions } from "./transports.js";
6
6
  export { DatadogReportingSink, DatadogReportingSinkOptions, GrafanaLokiReportingSink, GrafanaLokiReportingSinkOptions, InfluxDbReportingSink, InfluxDbReportingSinkOptions, OtelCollectorReportingSink, OtelCollectorReportingSinkOptions, SplunkReportingSink, SplunkReportingSinkOptions, TimescaleDbReportingSink, TimescaleDbReportingSinkOptions } from "./sinks.js";
package/dist/esm/local.js CHANGED
@@ -5,7 +5,6 @@ import { createHash, createVerify, randomUUID } from "node:crypto";
5
5
  import { CorrelationStoreConfiguration, CrossPlatformTrackingRuntime, RedisCorrelationStoreOptions, RedisCorrelationStore, TrackingFieldSelector } from "./correlation.js";
6
6
  import { EndpointAdapterFactory, LOADSTRIKE_TRACE_ID_TRACKING_FIELD } from "./transports.js";
7
7
  const DEFAULT_LICENSING_API_BASE_URL = "https://licensing.loadstrike.com";
8
- const INTERNAL_BLACKBOX_LICENSING_API_BASE_URL_ENVIRONMENT_VARIABLE = "LOADSTRIKE_INTERNAL_BLACKBOX_API_BASE_URL";
9
8
  let developmentLicensingApiBaseUrlOverride;
10
9
  const BUILT_IN_WORKER_PLUGIN_NAMES = new Set([
11
10
  "loadstrike failed responses",
@@ -27,7 +26,8 @@ const TRACKING_FEATURE_BY_KIND = {
27
26
  pushdiffusion: "endpoint.push_diffusion",
28
27
  delegatestream: "endpoint.delegate_stream",
29
28
  nats: "endpoint.nats",
30
- redisstreams: "endpoint.redis_streams"
29
+ redisstreams: "endpoint.redis_streams",
30
+ sqs: "endpoint.sqs"
31
31
  };
32
32
  const CI_ENVIRONMENT_VARIABLES = [
33
33
  "GITHUB_ACTIONS",
@@ -466,25 +466,8 @@ function normalizeLicensingApiBaseUrl(value) {
466
466
  const normalized = (value ?? "").trim();
467
467
  return normalized || DEFAULT_LICENSING_API_BASE_URL;
468
468
  }
469
- function resolveInternalBlackboxLicensingApiBaseUrlOverride(value) {
470
- const normalized = String(value ?? "").trim();
471
- if (!normalized) {
472
- return undefined;
473
- }
474
- try {
475
- const parsed = new URL(normalized);
476
- const host = parsed.hostname.replace(/^\[|\]$/g, "").toLowerCase();
477
- if (["http:", "https:"].includes(parsed.protocol) && ["127.0.0.1", "localhost", "::1"].includes(host)) {
478
- return normalized.replace(/\/+$/, "");
479
- }
480
- }
481
- catch {
482
- return undefined;
483
- }
484
- return undefined;
485
- }
486
469
  function resolveLicensingApiBaseUrl() {
487
- return normalizeLicensingApiBaseUrl(resolveInternalBlackboxLicensingApiBaseUrlOverride(process.env[INTERNAL_BLACKBOX_LICENSING_API_BASE_URL_ENVIRONMENT_VARIABLE]) ?? developmentLicensingApiBaseUrlOverride);
470
+ return normalizeLicensingApiBaseUrl(developmentLicensingApiBaseUrlOverride);
488
471
  }
489
472
  function setDevelopmentLicensingApiBaseUrlOverride(value) {
490
473
  developmentLicensingApiBaseUrlOverride = value;
@@ -523,6 +506,9 @@ function collectRequestedFeatures(request) {
523
506
  if (countCustomWorkerPlugins(context) > 0) {
524
507
  features.add("extensions.worker_plugins.custom");
525
508
  }
509
+ if (asList(pickValue(context, "RuntimePolicies", "runtimePolicies")).length > 0) {
510
+ features.add("policy.runtime_controls");
511
+ }
526
512
  const reportingSinks = asList(pickValue(context, "ReportingSinks", "reportingSinks"));
527
513
  let hasCustomSink = false;
528
514
  for (const sink of reportingSinks) {
@@ -818,159 +804,171 @@ async function evaluateScenarioOutcome(scenario, requestCount, context) {
818
804
  validateTrackingConfiguration(tracking, sourceEndpoint, destinationEndpoint);
819
805
  const sourceAdapter = EndpointAdapterFactory.create(sourceEndpoint);
820
806
  const destinationAdapter = destinationEndpoint ? EndpointAdapterFactory.create(destinationEndpoint) : null;
821
- const correlationTimeoutOverride = pickValue(tracking, "CorrelationTimeoutMs", "correlationTimeoutMs");
822
- const correlationTimeoutSeconds = pickValue(tracking, "CorrelationTimeoutSeconds", "correlationTimeoutSeconds", "CorrelationTimeout");
823
- const timeoutMs = correlationTimeoutOverride != null && String(correlationTimeoutOverride).trim() !== ""
824
- ? asInt(correlationTimeoutOverride)
825
- : Math.trunc((correlationTimeoutSeconds == null || String(correlationTimeoutSeconds).trim() === ""
826
- ? 30
827
- : asNumber(correlationTimeoutSeconds)) * 1000);
828
- const timeoutCountsAsFailure = toBoolean(pickValue(tracking, "TimeoutCountsAsFailure", "timeoutCountsAsFailure"), true);
829
- const correlationStore = mapCorrelationStore(tracking, buildTrackingRunNamespace(stringOrDefault(pickValue(context, "SessionId", "sessionId"), "session"), stringOrDefault(pickValue(scenario, "Name", "name"), "scenario"), sourceEndpoint.name, destinationEndpoint?.name));
830
- const timeoutSweepIntervalOverride = pickValue(tracking, "TimeoutSweepIntervalMs", "timeoutSweepIntervalMs");
831
- const timeoutSweepIntervalSeconds = pickValue(tracking, "TimeoutSweepIntervalSeconds", "timeoutSweepIntervalSeconds");
832
- const timeoutSweepIntervalMs = timeoutSweepIntervalOverride != null && String(timeoutSweepIntervalOverride).trim() !== ""
833
- ? asInt(timeoutSweepIntervalOverride)
834
- : Math.trunc((timeoutSweepIntervalSeconds == null || String(timeoutSweepIntervalSeconds).trim() === ""
835
- ? 1
836
- : asNumber(timeoutSweepIntervalSeconds)) * 1000);
837
- const timeoutBatchSizeValue = pickValue(tracking, "TimeoutBatchSize", "timeoutBatchSize");
838
- const timeoutBatchSize = timeoutBatchSizeValue == null || String(timeoutBatchSizeValue).trim() === ""
839
- ? 200
840
- : asInt(timeoutBatchSizeValue);
841
- const runtime = new CrossPlatformTrackingRuntime({
842
- sourceTrackingField: sourceEndpoint.trackingField,
843
- destinationTrackingField: destinationEndpoint?.trackingField,
844
- destinationGatherByField: destinationEndpoint?.gatherByField,
845
- trackingFieldValueCaseSensitive: toBoolean(pickValue(tracking, "TrackingFieldValueCaseSensitive", "trackingFieldValueCaseSensitive"), true),
846
- gatherByFieldValueCaseSensitive: toBoolean(pickValue(tracking, "GatherByFieldValueCaseSensitive", "gatherByFieldValueCaseSensitive"), true),
847
- correlationTimeoutMs: timeoutMs,
848
- timeoutCountsAsFailure,
849
- store: correlationStore ?? undefined
850
- });
851
- const runMode = stringOrDefault(pickValue(tracking, "RunMode", "runMode"), "GenerateAndCorrelate").trim().toLowerCase();
852
- const sourceOnlyMode = !destinationEndpoint;
853
- const restartOnFail = toBoolean(pickValue(scenario, "RestartIterationOnFail", "restartIterationOnFail"), false);
854
- const restartMaxAttempts = Math.max(asInt(pickValue(context, "RestartIterationMaxAttempts", "restartIterationMaxAttempts")), 0);
855
- const maxFailCount = Math.max(asInt(pickValue(scenario, "MaxFailCount", "maxFailCount")), 0);
856
- const scenarioCompletionTimeoutSeconds = Math.max(asNumber(pickValue(context, "ScenarioCompletionTimeoutSeconds", "scenarioCompletionTimeoutSeconds")), 0);
857
- const scenarioDeadlineMs = scenarioCompletionTimeoutSeconds > 0
858
- ? Date.now() + Math.trunc(scenarioCompletionTimeoutSeconds * 1000)
859
- : Number.POSITIVE_INFINITY;
860
- let okCount = 0;
861
- let failCount = 0;
862
- let nowMs = Date.now();
863
- let processedIterations = 0;
864
- let lastSweepAtMs = nowMs;
865
- for (let i = 0; i < requestCount; i += 1) {
866
- if (Date.now() > scenarioDeadlineMs) {
867
- break;
868
- }
869
- let iterationComplete = false;
870
- let attempts = 0;
871
- const maxAttempts = 1 + (restartOnFail ? restartMaxAttempts : 0);
872
- while (!iterationComplete) {
873
- attempts += 1;
807
+ let correlationStore = null;
808
+ try {
809
+ await sourceAdapter.initialize?.();
810
+ await destinationAdapter?.initialize?.();
811
+ const correlationTimeoutOverride = pickValue(tracking, "CorrelationTimeoutMs", "correlationTimeoutMs");
812
+ const correlationTimeoutSeconds = pickValue(tracking, "CorrelationTimeoutSeconds", "correlationTimeoutSeconds", "CorrelationTimeout");
813
+ const timeoutMs = correlationTimeoutOverride != null && String(correlationTimeoutOverride).trim() !== ""
814
+ ? asInt(correlationTimeoutOverride)
815
+ : Math.trunc((correlationTimeoutSeconds == null || String(correlationTimeoutSeconds).trim() === ""
816
+ ? 30
817
+ : asNumber(correlationTimeoutSeconds)) * 1000);
818
+ const timeoutCountsAsFailure = toBoolean(pickValue(tracking, "TimeoutCountsAsFailure", "timeoutCountsAsFailure"), true);
819
+ correlationStore = mapCorrelationStore(tracking, buildTrackingRunNamespace(stringOrDefault(pickValue(context, "SessionId", "sessionId"), "session"), stringOrDefault(pickValue(scenario, "Name", "name"), "scenario"), sourceEndpoint.name, destinationEndpoint?.name));
820
+ const timeoutSweepIntervalOverride = pickValue(tracking, "TimeoutSweepIntervalMs", "timeoutSweepIntervalMs");
821
+ const timeoutSweepIntervalSeconds = pickValue(tracking, "TimeoutSweepIntervalSeconds", "timeoutSweepIntervalSeconds");
822
+ const timeoutSweepIntervalMs = timeoutSweepIntervalOverride != null && String(timeoutSweepIntervalOverride).trim() !== ""
823
+ ? asInt(timeoutSweepIntervalOverride)
824
+ : Math.trunc((timeoutSweepIntervalSeconds == null || String(timeoutSweepIntervalSeconds).trim() === ""
825
+ ? 1
826
+ : asNumber(timeoutSweepIntervalSeconds)) * 1000);
827
+ const timeoutBatchSizeValue = pickValue(tracking, "TimeoutBatchSize", "timeoutBatchSize");
828
+ const timeoutBatchSize = timeoutBatchSizeValue == null || String(timeoutBatchSizeValue).trim() === ""
829
+ ? 200
830
+ : asInt(timeoutBatchSizeValue);
831
+ const runtime = new CrossPlatformTrackingRuntime({
832
+ sourceTrackingField: sourceEndpoint.trackingField,
833
+ destinationTrackingField: destinationEndpoint?.trackingField,
834
+ destinationGatherByField: destinationEndpoint?.gatherByField,
835
+ trackingFieldValueCaseSensitive: toBoolean(pickValue(tracking, "TrackingFieldValueCaseSensitive", "trackingFieldValueCaseSensitive"), true),
836
+ gatherByFieldValueCaseSensitive: toBoolean(pickValue(tracking, "GatherByFieldValueCaseSensitive", "gatherByFieldValueCaseSensitive"), true),
837
+ correlationTimeoutMs: timeoutMs,
838
+ timeoutCountsAsFailure,
839
+ store: correlationStore ?? undefined
840
+ });
841
+ const runMode = stringOrDefault(pickValue(tracking, "RunMode", "runMode"), "GenerateAndCorrelate").trim().toLowerCase();
842
+ const sourceOnlyMode = !destinationEndpoint;
843
+ const restartOnFail = toBoolean(pickValue(scenario, "RestartIterationOnFail", "restartIterationOnFail"), false);
844
+ const restartMaxAttempts = Math.max(asInt(pickValue(context, "RestartIterationMaxAttempts", "restartIterationMaxAttempts")), 0);
845
+ const maxFailCount = Math.max(asInt(pickValue(scenario, "MaxFailCount", "maxFailCount")), 0);
846
+ const scenarioCompletionTimeoutSeconds = Math.max(asNumber(pickValue(context, "ScenarioCompletionTimeoutSeconds", "scenarioCompletionTimeoutSeconds")), 0);
847
+ const scenarioDeadlineMs = scenarioCompletionTimeoutSeconds > 0
848
+ ? Date.now() + Math.trunc(scenarioCompletionTimeoutSeconds * 1000)
849
+ : Number.POSITIVE_INFINITY;
850
+ let okCount = 0;
851
+ let failCount = 0;
852
+ let nowMs = Date.now();
853
+ let processedIterations = 0;
854
+ let lastSweepAtMs = nowMs;
855
+ for (let i = 0; i < requestCount; i += 1) {
874
856
  if (Date.now() > scenarioDeadlineMs) {
875
- iterationComplete = true;
876
857
  break;
877
858
  }
878
- let sourcePayload = runMode === "correlateexistingtraffic"
879
- ? await sourceAdapter.consume()
880
- : await sourceAdapter.produce();
881
- sourcePayload = normalizePayload(sourcePayload, sourceEndpoint, i);
882
- const sourceTrackingId = sourceOnlyMode
883
- ? readTrackingId(sourcePayload, sourceEndpoint.trackingField)
884
- : await runtime.onSourceProduced(sourcePayload, nowMs);
885
- if (!sourceTrackingId) {
886
- if (restartOnFail && attempts < maxAttempts) {
859
+ let iterationComplete = false;
860
+ let attempts = 0;
861
+ const maxAttempts = 1 + (restartOnFail ? restartMaxAttempts : 0);
862
+ while (!iterationComplete) {
863
+ attempts += 1;
864
+ if (Date.now() > scenarioDeadlineMs) {
865
+ iterationComplete = true;
866
+ break;
867
+ }
868
+ let sourcePayload = runMode === "correlateexistingtraffic"
869
+ ? await sourceAdapter.consume()
870
+ : await sourceAdapter.produce();
871
+ sourcePayload = normalizePayload(sourcePayload, sourceEndpoint, i);
872
+ const sourceTrackingId = sourceOnlyMode
873
+ ? readTrackingId(sourcePayload, sourceEndpoint.trackingField)
874
+ : await runtime.onSourceProduced(sourcePayload, nowMs);
875
+ if (!sourceTrackingId) {
876
+ if (restartOnFail && attempts < maxAttempts) {
877
+ nowMs += 1;
878
+ continue;
879
+ }
880
+ failCount += 1;
881
+ iterationComplete = true;
887
882
  nowMs += 1;
888
- continue;
883
+ break;
889
884
  }
890
- failCount += 1;
891
- iterationComplete = true;
892
- nowMs += 1;
893
- break;
894
- }
895
- if (sourceOnlyMode || !destinationAdapter || !destinationEndpoint) {
896
- okCount += 1;
897
- iterationComplete = true;
898
- nowMs += 1;
899
- break;
900
- }
901
- let destinationPayload = await destinationAdapter.consume();
902
- destinationPayload = normalizePayload(destinationPayload, destinationEndpoint, i);
903
- let matched = await runtime.onDestinationConsumed(destinationPayload, nowMs + 1);
904
- const correlationDeadlineMs = nowMs + timeoutMs;
905
- while (!matched && Date.now() <= scenarioDeadlineMs && nowMs < correlationDeadlineMs) {
906
- const remainingTimeoutMs = correlationDeadlineMs - nowMs;
907
- if (remainingTimeoutMs <= 0) {
885
+ if (sourceOnlyMode || !destinationAdapter || !destinationEndpoint) {
886
+ okCount += 1;
887
+ iterationComplete = true;
888
+ nowMs += 1;
908
889
  break;
909
890
  }
910
- await sleep(Math.min(destinationEndpoint.pollIntervalMs ?? 250, remainingTimeoutMs));
911
- destinationPayload = normalizePayload(await destinationAdapter.consume(), destinationEndpoint, i);
912
- matched = await runtime.onDestinationConsumed(destinationPayload, nowMs + 1);
891
+ let destinationPayload = await destinationAdapter.consume();
892
+ destinationPayload = normalizePayload(destinationPayload, destinationEndpoint, i);
893
+ let matched = await runtime.onDestinationConsumed(destinationPayload, nowMs + 1);
894
+ const correlationDeadlineMs = nowMs + timeoutMs;
895
+ while (!matched && Date.now() <= scenarioDeadlineMs && nowMs < correlationDeadlineMs) {
896
+ const remainingTimeoutMs = correlationDeadlineMs - nowMs;
897
+ if (remainingTimeoutMs <= 0) {
898
+ break;
899
+ }
900
+ await sleep(Math.min(destinationEndpoint.pollIntervalMs ?? 250, remainingTimeoutMs));
901
+ destinationPayload = normalizePayload(await destinationAdapter.consume(), destinationEndpoint, i);
902
+ matched = await runtime.onDestinationConsumed(destinationPayload, nowMs + 1);
903
+ if (matched) {
904
+ break;
905
+ }
906
+ nowMs += Math.max(destinationEndpoint.pollIntervalMs ?? 250, 1);
907
+ }
913
908
  if (matched) {
914
- break;
909
+ okCount += 1;
910
+ iterationComplete = true;
915
911
  }
916
- nowMs += Math.max(destinationEndpoint.pollIntervalMs ?? 250, 1);
917
- }
918
- if (matched) {
919
- okCount += 1;
920
- iterationComplete = true;
921
- }
922
- else if (restartOnFail && attempts < maxAttempts) {
923
- nowMs += 2;
924
- continue;
925
- }
926
- else {
927
- await runtime.sweepTimeouts(nowMs + timeoutMs + 1, timeoutBatchSize || undefined);
928
- failCount += 1;
929
- iterationComplete = true;
930
- }
931
- nowMs += 2;
932
- }
933
- processedIterations += 1;
934
- if (timeoutSweepIntervalMs > 0 && nowMs - lastSweepAtMs >= timeoutSweepIntervalMs) {
935
- const swept = await runtime.sweepTimeouts(nowMs, timeoutBatchSize || undefined);
936
- if (swept > 0) {
937
- if (timeoutCountsAsFailure) {
938
- failCount += swept;
912
+ else if (restartOnFail && attempts < maxAttempts) {
913
+ nowMs += 2;
914
+ continue;
939
915
  }
940
916
  else {
941
- okCount += swept;
917
+ await runtime.sweepTimeouts(nowMs + timeoutMs + 1, timeoutBatchSize || undefined);
918
+ failCount += 1;
919
+ iterationComplete = true;
942
920
  }
921
+ nowMs += 2;
922
+ }
923
+ processedIterations += 1;
924
+ if (timeoutSweepIntervalMs > 0 && nowMs - lastSweepAtMs >= timeoutSweepIntervalMs) {
925
+ const swept = await runtime.sweepTimeouts(nowMs, timeoutBatchSize || undefined);
926
+ if (swept > 0) {
927
+ if (timeoutCountsAsFailure) {
928
+ failCount += swept;
929
+ }
930
+ else {
931
+ okCount += swept;
932
+ }
933
+ }
934
+ lastSweepAtMs = nowMs;
935
+ }
936
+ if (maxFailCount > 0 && failCount >= maxFailCount) {
937
+ break;
943
938
  }
944
- lastSweepAtMs = nowMs;
945
- }
946
- if (maxFailCount > 0 && failCount >= maxFailCount) {
947
- break;
948
939
  }
949
- }
950
- let expired = 0;
951
- let sweptChunk = 0;
952
- do {
953
- sweptChunk = await runtime.sweepTimeouts(nowMs + timeoutMs + 1, timeoutBatchSize || undefined);
954
- expired += sweptChunk;
955
- } while (timeoutBatchSize > 0 && sweptChunk >= timeoutBatchSize);
956
- if (expired > 0) {
957
- if (timeoutCountsAsFailure) {
958
- failCount += expired;
940
+ let expired = 0;
941
+ let sweptChunk = 0;
942
+ do {
943
+ sweptChunk = await runtime.sweepTimeouts(nowMs + timeoutMs + 1, timeoutBatchSize || undefined);
944
+ expired += sweptChunk;
945
+ } while (timeoutBatchSize > 0 && sweptChunk >= timeoutBatchSize);
946
+ if (expired > 0) {
947
+ if (timeoutCountsAsFailure) {
948
+ failCount += expired;
949
+ }
950
+ else {
951
+ okCount += expired;
952
+ }
959
953
  }
960
- else {
961
- okCount += expired;
954
+ if (processedIterations < requestCount && Date.now() > scenarioDeadlineMs) {
955
+ const timedOutIterations = requestCount - processedIterations;
956
+ if (timeoutCountsAsFailure) {
957
+ failCount += timedOutIterations;
958
+ }
959
+ else {
960
+ okCount += timedOutIterations;
961
+ }
962
962
  }
963
+ return { requestCount: processedIterations, okCount, failCount };
963
964
  }
964
- if (processedIterations < requestCount && Date.now() > scenarioDeadlineMs) {
965
- const timedOutIterations = requestCount - processedIterations;
966
- if (timeoutCountsAsFailure) {
967
- failCount += timedOutIterations;
968
- }
969
- else {
970
- okCount += timedOutIterations;
971
- }
965
+ finally {
966
+ await Promise.allSettled([
967
+ sourceAdapter.dispose?.(),
968
+ destinationAdapter?.dispose?.(),
969
+ correlationStore?.close?.()
970
+ ].filter((value) => Boolean(value)));
972
971
  }
973
- return { requestCount: processedIterations, okCount, failCount };
974
972
  }
975
973
  function validateTrackingConfiguration(tracking, sourceEndpoint, destinationEndpoint) {
976
974
  if (sourceEndpoint.gatherByField?.trim()) {
@@ -1007,10 +1005,20 @@ function validateTrackingConfiguration(tracking, sourceEndpoint, destinationEndp
1007
1005
  if (!metricPrefix.trim()) {
1008
1006
  throw new Error("MetricPrefix must be provided.");
1009
1007
  }
1008
+ const observationDurationOverride = pickValue(tracking, "ObservationDurationMs", "observationDurationMs");
1009
+ const observationDurationSeconds = pickValue(tracking, "ObservationDurationSeconds", "observationDurationSeconds", "ObservationDuration");
1010
+ const observationDurationMs = observationDurationOverride != null && String(observationDurationOverride).trim() !== ""
1011
+ ? asNumber(observationDurationOverride)
1012
+ : (observationDurationSeconds == null || String(observationDurationSeconds).trim() === ""
1013
+ ? 0
1014
+ : asNumber(observationDurationSeconds) * 1000);
1010
1015
  validateRedisCorrelationStoreConfiguration(tracking);
1011
1016
  const runModeRaw = stringOrDefault(pickValue(tracking, "RunMode", "runMode"), "GenerateAndCorrelate").trim().toLowerCase();
1012
1017
  const runMode = runModeRaw === "sourceonly" ? "generateandcorrelate" : runModeRaw;
1013
1018
  if (runMode === "generateandcorrelate") {
1019
+ if (observationDurationMs > 0) {
1020
+ throw new Error("ForDuration is only supported for CorrelateExistingTraffic.");
1021
+ }
1014
1022
  if (sourceEndpoint.mode !== "Produce") {
1015
1023
  throw new Error("Source endpoint mode must be Produce for GenerateAndCorrelate mode.");
1016
1024
  }
@@ -1020,6 +1028,9 @@ function validateTrackingConfiguration(tracking, sourceEndpoint, destinationEndp
1020
1028
  return;
1021
1029
  }
1022
1030
  if (runMode === "correlateexistingtraffic") {
1031
+ if (observationDurationMs <= 0) {
1032
+ throw new Error("CorrelateExistingTraffic requires ForDuration with a duration greater than zero.");
1033
+ }
1023
1034
  if (!destinationEndpoint) {
1024
1035
  throw new Error("Destination endpoint must be provided for CorrelateExistingTraffic mode.");
1025
1036
  }