@loadstrike/loadstrike-sdk 1.0.30001 → 1.0.30401

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.
@@ -0,0 +1,711 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.IterationObservationReporter = exports.ProcessIterationObservationBuffer = exports.DEFAULT_ITERATION_OBSERVATION_SETTINGS = exports.ITERATION_OBSERVATION_STREAM_COMPLETION_SCHEMA_VERSION = exports.ITERATION_OBSERVATION_BATCH_SCHEMA_VERSION = exports.ITERATION_OBSERVATION_SCHEMA_VERSION = void 0;
4
+ exports.utcNowNs = utcNowNs;
5
+ exports.deriveIterationId = deriveIterationId;
6
+ exports.deriveObservationId = deriveObservationId;
7
+ exports.deriveIterationBatchId = deriveIterationBatchId;
8
+ exports.createIterationStepObservation = createIterationStepObservation;
9
+ exports.createIterationObservation = createIterationObservation;
10
+ exports.serializeIterationObservationBatchGzipJson = serializeIterationObservationBatchGzipJson;
11
+ exports.validateIterationObservationSettings = validateIterationObservationSettings;
12
+ const node_crypto_1 = require("node:crypto");
13
+ const node_zlib_1 = require("node:zlib");
14
+ exports.ITERATION_OBSERVATION_SCHEMA_VERSION = "loadstrike.iteration-observation/1";
15
+ exports.ITERATION_OBSERVATION_BATCH_SCHEMA_VERSION = "loadstrike.iteration-batch/1";
16
+ exports.ITERATION_OBSERVATION_STREAM_COMPLETION_SCHEMA_VERSION = "loadstrike.iteration-stream-completion/1";
17
+ exports.DEFAULT_ITERATION_OBSERVATION_SETTINGS = Object.freeze({
18
+ flushIntervalMs: 5000,
19
+ maxBufferBytes: 256 * 1024 * 1024,
20
+ maxObservationsPerBatch: 50000,
21
+ maxBatchBytes: 8 * 1024 * 1024,
22
+ sinkQueueDepth: 4,
23
+ sinkParallelism: 2,
24
+ drainTimeoutMs: 30000
25
+ });
26
+ const BATCH_ENVELOPE_RESERVED_BYTES = 2048;
27
+ const defaultClock = {
28
+ nowUtcNs: utcNowNs,
29
+ setInterval(callback, intervalMs) {
30
+ const timer = setInterval(callback, intervalMs);
31
+ timer.unref?.();
32
+ return timer;
33
+ },
34
+ clearInterval(handle) {
35
+ clearInterval(handle);
36
+ }
37
+ };
38
+ function utcNowNs() {
39
+ return BigInt(Date.now()) * 1000000n;
40
+ }
41
+ function deriveIterationId(runId, resultOwnerId, scenarioName, simulationIndex, globalOrdinal64) {
42
+ return derivePrefixedSha256("lsi_", `${runId}\n${resultOwnerId}\n${scenarioName}\n${Math.trunc(simulationIndex)}\n${decimalString(globalOrdinal64)}`);
43
+ }
44
+ function deriveObservationId(iterationId, attemptIndex) {
45
+ return derivePrefixedSha256("lso_", `${iterationId}\n${Math.max(Math.trunc(attemptIndex), 0)}`);
46
+ }
47
+ function deriveIterationBatchId(runId, resultOwnerId, batchSequence64) {
48
+ return derivePrefixedSha256("lsb_", `${runId}\n${resultOwnerId}\n${decimalString(batchSequence64)}`);
49
+ }
50
+ function createIterationStepObservation(input) {
51
+ return {
52
+ stepName: String(input.stepName),
53
+ sortIndex: Math.max(Math.trunc(input.sortIndex), 0),
54
+ startedUtcNs: nonNegativeDecimalString(input.startedUtcNs),
55
+ completedUtcNs: nonNegativeDecimalString(input.completedUtcNs),
56
+ observedLatencyUs64: nonNegativeDecimalString(input.observedLatencyUs64),
57
+ reportedLatencyUs64: nonNegativeDecimalString(input.reportedLatencyUs64),
58
+ isSuccess: Boolean(input.isSuccess),
59
+ statusCode: truncateUtf8(String(input.statusCode ?? ""), 64),
60
+ sizeBytes64: nonNegativeDecimalString(input.sizeBytes64)
61
+ };
62
+ }
63
+ function createIterationObservation(input) {
64
+ const globalOrdinal64 = nonNegativeDecimalString(input.globalOrdinal64);
65
+ const globalSecondaryOrdinal64 = nonNegativeDecimalString(input.globalSecondaryOrdinal64 ?? 0);
66
+ const simulationIndex = Math.trunc(input.simulationIndex);
67
+ const attemptIndex = Math.max(Math.trunc(input.attemptIndex), 0);
68
+ const iterationId = input.iterationId ?? deriveIterationId(String(input.runId), String(input.resultOwnerId), String(input.scenarioName), simulationIndex, globalOrdinal64);
69
+ if (!iterationId) {
70
+ throw new Error("Iteration identity must not be empty.");
71
+ }
72
+ return {
73
+ schemaVersion: exports.ITERATION_OBSERVATION_SCHEMA_VERSION,
74
+ observationId: deriveObservationId(iterationId, attemptIndex),
75
+ runId: String(input.runId),
76
+ sessionId: String(input.sessionId),
77
+ resultOwnerId: String(input.resultOwnerId),
78
+ processGroup: Math.max(Math.trunc(input.processGroup), 0),
79
+ scenarioName: String(input.scenarioName),
80
+ scenarioIndex: Math.max(Math.trunc(input.scenarioIndex), 0),
81
+ simulationIndex,
82
+ simulationKind: String(input.simulationKind || "SingleInvocation"),
83
+ phase: input.phase === "warmup" ? "warmup" : "bombing",
84
+ globalOrdinal64,
85
+ globalSecondaryOrdinal64,
86
+ shardIndex: Math.max(Math.trunc(input.shardIndex), 0),
87
+ shardCount: Math.max(Math.trunc(input.shardCount), 1),
88
+ iterationId,
89
+ attemptIndex,
90
+ isFinalAttempt: Boolean(input.isFinalAttempt),
91
+ startedUtcNs: nonNegativeDecimalString(input.startedUtcNs),
92
+ completedUtcNs: nonNegativeDecimalString(input.completedUtcNs),
93
+ observedLatencyUs64: nonNegativeDecimalString(input.observedLatencyUs64),
94
+ reportedLatencyUs64: nonNegativeDecimalString(input.reportedLatencyUs64),
95
+ isSuccess: Boolean(input.isSuccess),
96
+ statusCode: truncateUtf8(String(input.statusCode ?? ""), 64),
97
+ sizeBytes64: nonNegativeDecimalString(input.sizeBytes64),
98
+ steps: input.steps.map((step) => createIterationStepObservation(step))
99
+ };
100
+ }
101
+ function serializeIterationObservationBatchGzipJson(value) {
102
+ return (0, node_zlib_1.gzipSync)(Buffer.from(JSON.stringify(value), "utf8"));
103
+ }
104
+ class ProcessIterationObservationBuffer {
105
+ constructor() {
106
+ this.streams = new Map();
107
+ this.streamLimits = new Map();
108
+ this.totalBytes = 0;
109
+ }
110
+ get byteLength() {
111
+ return this.totalBytes;
112
+ }
113
+ register(streamKey, maxBufferBytes) {
114
+ this.streamLimits.set(streamKey, maxBufferBytes);
115
+ }
116
+ offer(streamKey, observation, maxBufferBytes, encodedBytes) {
117
+ this.streamLimits.set(streamKey, maxBufferBytes);
118
+ const processLimit = this.effectiveLimit(maxBufferBytes);
119
+ const jsonBytes = encodedBytes ?? Buffer.byteLength(JSON.stringify(observation), "utf8");
120
+ if (jsonBytes > processLimit || this.totalBytes + jsonBytes > processLimit) {
121
+ return false;
122
+ }
123
+ const stream = this.streams.get(streamKey) ?? [];
124
+ stream.push({ observation, jsonBytes });
125
+ this.streams.set(streamKey, stream);
126
+ this.totalBytes += jsonBytes;
127
+ return true;
128
+ }
129
+ drain(streamKey) {
130
+ const stream = this.streams.get(streamKey) ?? [];
131
+ this.streams.delete(streamKey);
132
+ for (const item of stream) {
133
+ this.totalBytes -= item.jsonBytes;
134
+ }
135
+ this.totalBytes = Math.max(this.totalBytes, 0);
136
+ return stream;
137
+ }
138
+ release(streamKey) {
139
+ this.drain(streamKey);
140
+ }
141
+ unregister(streamKey) {
142
+ this.drain(streamKey);
143
+ this.streamLimits.delete(streamKey);
144
+ }
145
+ effectiveLimit(fallback) {
146
+ let limit = fallback;
147
+ for (const value of this.streamLimits.values()) {
148
+ limit = Math.min(limit, value);
149
+ }
150
+ return limit;
151
+ }
152
+ }
153
+ exports.ProcessIterationObservationBuffer = ProcessIterationObservationBuffer;
154
+ const processIterationObservationBuffer = new ProcessIterationObservationBuffer();
155
+ class SinkDispatcher {
156
+ constructor(target, depth, parallelism, onDropped) {
157
+ this.target = target;
158
+ this.depth = depth;
159
+ this.parallelism = parallelism;
160
+ this.onDropped = onDropped;
161
+ this.queue = [];
162
+ this.active = new Set();
163
+ this.idleWaiters = new Set();
164
+ this.timedOut = false;
165
+ this.delivered = 0n;
166
+ this.dropped = 0n;
167
+ }
168
+ get deliveredCount() {
169
+ return this.delivered;
170
+ }
171
+ get droppedCount() {
172
+ return this.dropped;
173
+ }
174
+ get canPublishCompletion() {
175
+ return this.queue.length === 0 && this.active.size === 0;
176
+ }
177
+ enqueue(batch) {
178
+ if (this.timedOut || this.queue.length >= this.depth) {
179
+ this.drop("observation_sink_overflow", batch.observations);
180
+ return false;
181
+ }
182
+ this.queue.push({ batch, count: BigInt(batch.observations.length) });
183
+ this.pump();
184
+ return true;
185
+ }
186
+ async waitForIdle(timeoutMs) {
187
+ if (this.isIdle()) {
188
+ return true;
189
+ }
190
+ let timer;
191
+ let waiter;
192
+ const idle = new Promise((resolve) => {
193
+ waiter = () => {
194
+ if (timer)
195
+ clearTimeout(timer);
196
+ this.idleWaiters.delete(waiter);
197
+ resolve(true);
198
+ };
199
+ this.idleWaiters.add(waiter);
200
+ });
201
+ const timeout = new Promise((resolve) => {
202
+ timer = setTimeout(() => resolve(false), Math.max(timeoutMs, 1));
203
+ timer.unref?.();
204
+ });
205
+ const completed = await Promise.race([idle, timeout]);
206
+ if (timer) {
207
+ clearTimeout(timer);
208
+ }
209
+ if (!completed) {
210
+ if (waiter)
211
+ this.idleWaiters.delete(waiter);
212
+ this.timeoutOutstanding();
213
+ }
214
+ return completed;
215
+ }
216
+ pump() {
217
+ while (!this.timedOut && this.active.size < this.parallelism && this.queue.length > 0) {
218
+ const item = this.queue.shift();
219
+ this.active.add(item);
220
+ Promise.resolve()
221
+ .then(() => this.target.saveIterationBatch(item.batch))
222
+ .then(() => {
223
+ if (!this.timedOut) {
224
+ this.delivered += item.count;
225
+ }
226
+ })
227
+ .catch(() => {
228
+ if (!this.timedOut) {
229
+ this.drop("observation_sink_delivery_failed", item.batch.observations);
230
+ }
231
+ })
232
+ .finally(() => {
233
+ this.active.delete(item);
234
+ this.pump();
235
+ this.notifyIdle();
236
+ });
237
+ }
238
+ this.notifyIdle();
239
+ }
240
+ timeoutOutstanding() {
241
+ if (this.timedOut || this.isIdle())
242
+ return;
243
+ this.timedOut = true;
244
+ const queued = [...this.queue];
245
+ const active = [...this.active];
246
+ this.queue.length = 0;
247
+ for (const item of queued) {
248
+ this.drop("observation_sink_drain_timeout", item.batch.observations);
249
+ }
250
+ for (const item of active) {
251
+ // Active sink I/O cannot be cancelled safely. Warn, but settle its actual
252
+ // outcome before the completion marker and do not claim it was dropped.
253
+ this.onDropped("observation_sink_drain_timeout", this.target.name, item.batch.observations);
254
+ }
255
+ this.notifyIdle();
256
+ }
257
+ drop(code, observations) {
258
+ this.dropped += BigInt(observations.length);
259
+ this.onDropped(code, this.target.name, observations);
260
+ }
261
+ isIdle() {
262
+ return this.queue.length === 0 && this.active.size === 0;
263
+ }
264
+ notifyIdle(force = false) {
265
+ if (!force && !this.isIdle())
266
+ return;
267
+ for (const waiter of this.idleWaiters) {
268
+ waiter();
269
+ }
270
+ this.idleWaiters.clear();
271
+ }
272
+ }
273
+ class IterationObservationReporter {
274
+ constructor(options) {
275
+ this.options = options;
276
+ this.streamKey = Symbol("loadstrike-iteration-observations");
277
+ this.warnings = new Map();
278
+ this.sequence = 0n;
279
+ this.captured = 0n;
280
+ this.droppedBuffer = 0n;
281
+ this.unsupportedSinkCount = 0;
282
+ this.sealed = false;
283
+ this.finalResult = null;
284
+ this.expectedResultOwnerCount64 = positiveCanonicalDecimalString(options.expectedResultOwnerCount64 ?? "1", "Expected result owner count");
285
+ const requestedSettings = {
286
+ ...exports.DEFAULT_ITERATION_OBSERVATION_SETTINGS,
287
+ ...(options.settings ?? {})
288
+ };
289
+ const hasPortalSink = options.sinks.some((sink) => sink.iterationObservationPortalSink === true);
290
+ this.settings = hasPortalSink
291
+ ? {
292
+ ...requestedSettings,
293
+ maxObservationsPerBatch: Math.min(requestedSettings.maxObservationsPerBatch, 50000),
294
+ maxBatchBytes: Math.min(requestedSettings.maxBatchBytes, 8 * 1024 * 1024)
295
+ }
296
+ : requestedSettings;
297
+ if (!options.skipValidation) {
298
+ validateIterationObservationSettings(this.settings);
299
+ }
300
+ this.buffer = options.buffer ?? processIterationObservationBuffer;
301
+ this.clock = options.clock ?? defaultClock;
302
+ this.batchEnvelopeBytes = maximumIterationObservationBatchEnvelopeBytes(options);
303
+ this.dispatchers = options.sinks
304
+ .filter((sink) => {
305
+ if (typeof sink.saveIterationBatch === "function") {
306
+ if (sink.iterationObservationShapeLimited === true) {
307
+ this.recordWarning("sink_iteration_shape_limited", sink.name, "", -1, 1n, "The reporting sink stores a metric projection rather than the complete raw iteration shape.");
308
+ }
309
+ return true;
310
+ }
311
+ this.unsupportedSinkCount += 1;
312
+ this.recordWarning("sink_iteration_batches_unsupported", sink.name, "", -1, 1n, "The reporting sink does not support raw iteration batches.");
313
+ return false;
314
+ })
315
+ .map((target) => new SinkDispatcher(target, this.settings.sinkQueueDepth, this.settings.sinkParallelism, (code, sinkName, observations) => this.recordSinkDrop(code, sinkName, observations)));
316
+ if (this.dispatchers.length > 0) {
317
+ this.buffer.register(this.streamKey, this.settings.maxBufferBytes);
318
+ }
319
+ this.timer = this.clock.setInterval(() => this.flushNow(), this.settings.flushIntervalMs);
320
+ }
321
+ get enabled() {
322
+ return this.dispatchers.length > 0 && !this.sealed;
323
+ }
324
+ capture(observation) {
325
+ if (!this.enabled) {
326
+ return false;
327
+ }
328
+ const singleObservationBudget = Math.max(this.settings.maxBatchBytes - this.batchEnvelopeBytes, 0);
329
+ const upperBound = jsonUtf8SizeUpperBound(observation);
330
+ const encodedBytes = upperBound >= Math.floor(singleObservationBudget * 0.9)
331
+ ? Buffer.byteLength(JSON.stringify(observation), "utf8")
332
+ : upperBound;
333
+ if (encodedBytes > singleObservationBudget) {
334
+ this.droppedBuffer += 1n;
335
+ this.recordWarning("observation_batch_oversize", undefined, observation.scenarioName, observation.simulationIndex, 1n, "The canonical raw observation cannot fit within one configured batch.", observation.scenarioIndex, observation.simulationKind);
336
+ return false;
337
+ }
338
+ const immutable = deepFreezeObservation(observation);
339
+ if (this.buffer.offer(this.streamKey, immutable, this.settings.maxBufferBytes, encodedBytes)) {
340
+ this.captured += 1n;
341
+ return true;
342
+ }
343
+ this.droppedBuffer += 1n;
344
+ this.recordWarning("observation_buffer_overflow", undefined, immutable.scenarioName, immutable.simulationIndex, 1n, "The process-wide raw observation buffer was full.", immutable.scenarioIndex, immutable.simulationKind);
345
+ return false;
346
+ }
347
+ flushNow() {
348
+ if (this.dispatchers.length === 0) {
349
+ this.buffer.release(this.streamKey);
350
+ return;
351
+ }
352
+ const observations = this.buffer.drain(this.streamKey);
353
+ if (!observations.length) {
354
+ return;
355
+ }
356
+ const chunks = splitBufferedObservations(observations, this.settings.maxObservationsPerBatch, this.settings.maxBatchBytes, Math.max(BATCH_ENVELOPE_RESERVED_BYTES, this.batchEnvelopeBytes));
357
+ for (const chunk of chunks) {
358
+ const batch = this.createBatch(chunk.map((item) => item.observation), this.droppedBuffer);
359
+ for (const dispatcher of this.dispatchers) {
360
+ dispatcher.enqueue(batch);
361
+ }
362
+ }
363
+ }
364
+ async sealAndDrain() {
365
+ if (this.finalResult) {
366
+ return { ...this.finalResult };
367
+ }
368
+ this.sealed = true;
369
+ this.clock.clearInterval(this.timer);
370
+ this.flushNow();
371
+ const started = Date.now();
372
+ const deadline = started + this.settings.drainTimeoutMs;
373
+ const drainResults = new Array(this.dispatchers.length).fill(false);
374
+ const completionResults = await Promise.all(this.dispatchers.map(async (dispatcher, index) => {
375
+ const drained = await dispatcher.waitForIdle(Math.max(deadline - Date.now(), 1));
376
+ drainResults[index] = drained;
377
+ if (!dispatcher.canPublishCompletion) {
378
+ return false;
379
+ }
380
+ const complete = dispatcher.target.completeIterationObservationStream;
381
+ if (!complete) {
382
+ this.recordWarning("sink_iteration_completion_unsupported", dispatcher.target.name, "", -1, 1n, "The reporting sink does not support raw iteration stream completion markers.");
383
+ return false;
384
+ }
385
+ const sinkReportingComplete = drained
386
+ && this.droppedBuffer === 0n
387
+ && dispatcher.droppedCount === 0n
388
+ && dispatcher.deliveredCount === this.captured;
389
+ const completion = this.createCompletion(dispatcher, sinkReportingComplete);
390
+ let timer;
391
+ const outcome = await Promise.race([
392
+ Promise.resolve()
393
+ .then(() => complete(completion))
394
+ .then(() => "success", () => "failed"),
395
+ new Promise((resolve) => {
396
+ timer = setTimeout(() => resolve("timeout"), Math.max(deadline - Date.now(), 1));
397
+ timer.unref?.();
398
+ })
399
+ ]);
400
+ if (timer) {
401
+ clearTimeout(timer);
402
+ }
403
+ if (outcome === "success") {
404
+ return true;
405
+ }
406
+ this.recordWarning("observation_stream_completion_failed", dispatcher.target.name, "", -1, 1n, outcome === "timeout"
407
+ ? "The reporting sink did not complete its observation stream marker before shutdown."
408
+ : "The reporting sink did not accept the observation stream completion marker.");
409
+ return false;
410
+ }));
411
+ const reportingComplete = this.dispatchers.length === 0
412
+ ? this.unsupportedSinkCount === 0
413
+ : this.unsupportedSinkCount === 0
414
+ && drainResults.every(Boolean)
415
+ && completionResults.every(Boolean)
416
+ && this.droppedBuffer === 0n
417
+ && this.dispatchers.every((dispatcher) => dispatcher.droppedCount === 0n)
418
+ && this.dispatchers.every((dispatcher) => dispatcher.deliveredCount === this.captured);
419
+ const delivered = this.dispatchers.length
420
+ ? this.dispatchers.reduce((minimum, dispatcher) => dispatcher.deliveredCount < minimum
421
+ ? dispatcher.deliveredCount
422
+ : minimum, this.dispatchers[0].deliveredCount)
423
+ : 0n;
424
+ const droppedSink = this.dispatchers.reduce((maximum, dispatcher) => dispatcher.droppedCount > maximum
425
+ ? dispatcher.droppedCount
426
+ : maximum, 0n);
427
+ this.buffer.unregister(this.streamKey);
428
+ this.finalResult = {
429
+ lastBatchSequence64: this.sequence === 0n ? "-1" : (this.sequence - 1n).toString(),
430
+ capturedCount64: this.captured.toString(),
431
+ deliveredCount64: delivered.toString(),
432
+ droppedBufferCount64: this.droppedBuffer.toString(),
433
+ droppedSinkCount64: droppedSink.toString(),
434
+ reportingComplete
435
+ };
436
+ return { ...this.finalResult };
437
+ }
438
+ buildWarnings() {
439
+ return Array.from(this.warnings.values())
440
+ .sort((left, right) => left.code.localeCompare(right.code)
441
+ || String(left.sinkName ?? "").localeCompare(String(right.sinkName ?? ""))
442
+ || left.scenarioName.localeCompare(right.scenarioName)
443
+ || left.simulationIndex - right.simulationIndex)
444
+ .map((warning) => ({
445
+ code: warning.code,
446
+ ...(warning.sinkName ? { sinkName: warning.sinkName } : {}),
447
+ scenarioName: warning.scenarioName,
448
+ scenarioIndex: warning.scenarioIndex,
449
+ simulationIndex: warning.simulationIndex,
450
+ simulationKind: warning.simulationKind,
451
+ count64: warning.count.toString(),
452
+ message: warning.message,
453
+ firstObservedUtcNs: warning.firstObservedUtcNs.toString(),
454
+ lastObservedUtcNs: warning.lastObservedUtcNs.toString()
455
+ }));
456
+ }
457
+ createBatch(observations, droppedBeforeBatch) {
458
+ const sequence = this.sequence;
459
+ this.sequence += 1n;
460
+ const firstObservationUtcNs = observations.reduce((minimum, observation) => {
461
+ const value = BigInt(observation.startedUtcNs);
462
+ return value < minimum ? value : minimum;
463
+ }, BigInt(observations[0].startedUtcNs));
464
+ const lastObservationUtcNs = observations.reduce((maximum, observation) => {
465
+ const value = BigInt(observation.completedUtcNs);
466
+ return value > maximum ? value : maximum;
467
+ }, BigInt(observations[0].completedUtcNs));
468
+ return deepFreezeBatch({
469
+ schemaVersion: exports.ITERATION_OBSERVATION_BATCH_SCHEMA_VERSION,
470
+ batchId: deriveIterationBatchId(this.options.runId, this.options.resultOwnerId, sequence),
471
+ runId: this.options.runId,
472
+ sessionId: this.options.sessionId,
473
+ resultOwnerId: this.options.resultOwnerId,
474
+ processGroup: Math.max(Math.trunc(this.options.processGroup), 0),
475
+ batchSequence64: sequence.toString(),
476
+ createdUtcNs: this.clock.nowUtcNs().toString(),
477
+ firstObservationUtcNs: firstObservationUtcNs.toString(),
478
+ lastObservationUtcNs: lastObservationUtcNs.toString(),
479
+ observations,
480
+ capturedCount64: this.captured.toString(),
481
+ droppedBeforeBatch64: droppedBeforeBatch.toString(),
482
+ compression: "gzip-json"
483
+ });
484
+ }
485
+ createCompletion(dispatcher, reportingComplete) {
486
+ return Object.freeze({
487
+ schemaVersion: exports.ITERATION_OBSERVATION_STREAM_COMPLETION_SCHEMA_VERSION,
488
+ runId: this.options.runId,
489
+ sessionId: this.options.sessionId,
490
+ resultOwnerId: this.options.resultOwnerId,
491
+ expectedResultOwnerCount64: this.expectedResultOwnerCount64,
492
+ processGroup: Math.max(Math.trunc(this.options.processGroup), 0),
493
+ lastBatchSequence64: this.sequence === 0n ? "-1" : (this.sequence - 1n).toString(),
494
+ capturedCount64: this.captured.toString(),
495
+ deliveredCount64: dispatcher.deliveredCount.toString(),
496
+ droppedBufferCount64: this.droppedBuffer.toString(),
497
+ droppedSinkCount64: dispatcher.droppedCount.toString(),
498
+ reportingComplete,
499
+ completedUtcNs: this.clock.nowUtcNs().toString()
500
+ });
501
+ }
502
+ recordSinkDrop(code, sinkName, observations) {
503
+ const grouped = new Map();
504
+ for (const observation of observations) {
505
+ const key = `${observation.scenarioName}\n${observation.simulationIndex}`;
506
+ const group = grouped.get(key) ?? {
507
+ scenarioName: observation.scenarioName,
508
+ scenarioIndex: observation.scenarioIndex,
509
+ simulationIndex: observation.simulationIndex,
510
+ simulationKind: observation.simulationKind,
511
+ count: 0n
512
+ };
513
+ group.count += 1n;
514
+ grouped.set(key, group);
515
+ }
516
+ for (const group of grouped.values()) {
517
+ this.recordWarning(code, sinkName, group.scenarioName, group.simulationIndex, group.count, code === "observation_sink_overflow"
518
+ ? "The reporting sink raw observation queue was full."
519
+ : code === "observation_sink_drain_timeout"
520
+ ? "The reporting sink raw observation queue did not drain before shutdown."
521
+ : "The reporting sink failed to accept a raw observation batch.", group.scenarioIndex, group.simulationKind);
522
+ }
523
+ }
524
+ recordWarning(code, sinkName, scenarioName, simulationIndex, count, message, scenarioIndex = -1, simulationKind = "") {
525
+ if (count <= 0n)
526
+ return;
527
+ const key = `${code}\n${sinkName ?? ""}\n${scenarioIndex}\n${simulationIndex}\n${simulationKind}`;
528
+ const now = this.clock?.nowUtcNs?.() ?? utcNowNs();
529
+ const existing = this.warnings.get(key);
530
+ if (existing) {
531
+ existing.count += count;
532
+ existing.lastObservedUtcNs = now;
533
+ return;
534
+ }
535
+ this.warnings.set(key, {
536
+ code,
537
+ sinkName,
538
+ scenarioName,
539
+ scenarioIndex,
540
+ simulationIndex,
541
+ simulationKind,
542
+ count,
543
+ message,
544
+ firstObservedUtcNs: now,
545
+ lastObservedUtcNs: now
546
+ });
547
+ }
548
+ }
549
+ exports.IterationObservationReporter = IterationObservationReporter;
550
+ function validateIterationObservationSettings(settings) {
551
+ validateIntegerRange(settings.flushIntervalMs, 1000, 60000, "Iteration observation flush interval");
552
+ validateIntegerRange(settings.maxBufferBytes, 16 * 1024 * 1024, 4 * 1024 * 1024 * 1024, "Iteration observation buffer bytes");
553
+ validateIntegerRange(settings.maxObservationsPerBatch, 100, 1000000, "Iteration observations per batch");
554
+ validateIntegerRange(settings.maxBatchBytes, 1024 * 1024, 64 * 1024 * 1024, "Iteration observation batch bytes");
555
+ validateIntegerRange(settings.sinkQueueDepth, 1, 64, "Iteration observation sink queue depth");
556
+ validateIntegerRange(settings.sinkParallelism, 1, 16, "Iteration observation sink parallelism");
557
+ validateIntegerRange(settings.drainTimeoutMs, 1000, 300000, "Iteration observation drain timeout");
558
+ }
559
+ function validateIntegerRange(value, minimum, maximum, name) {
560
+ if (!Number.isSafeInteger(value) || value < minimum || value > maximum) {
561
+ throw new RangeError(`${name} must be an integer from ${minimum} through ${maximum}.`);
562
+ }
563
+ }
564
+ function splitBufferedObservations(observations, maxCount, maxBytes, envelopeBytes) {
565
+ const chunks = [];
566
+ let current = [];
567
+ let currentBytes = envelopeBytes;
568
+ for (const observation of observations) {
569
+ const projectedBytes = currentBytes + observation.jsonBytes + (current.length ? 1 : 0);
570
+ if (current.length > 0 && (current.length >= maxCount || projectedBytes > maxBytes)) {
571
+ chunks.push(current);
572
+ current = [];
573
+ currentBytes = envelopeBytes;
574
+ }
575
+ current.push(observation);
576
+ currentBytes += observation.jsonBytes + (current.length > 1 ? 1 : 0);
577
+ }
578
+ if (current.length) {
579
+ chunks.push(current);
580
+ }
581
+ return chunks;
582
+ }
583
+ function maximumIterationObservationBatchEnvelopeBytes(options) {
584
+ const maximumDecimal = "9223372036854775807";
585
+ const value = {
586
+ schemaVersion: exports.ITERATION_OBSERVATION_BATCH_SCHEMA_VERSION,
587
+ batchId: deriveIterationBatchId(options.runId, options.resultOwnerId, maximumDecimal),
588
+ runId: options.runId,
589
+ sessionId: options.sessionId,
590
+ resultOwnerId: options.resultOwnerId,
591
+ processGroup: Math.max(Math.trunc(options.processGroup), 0),
592
+ batchSequence64: maximumDecimal,
593
+ createdUtcNs: maximumDecimal,
594
+ firstObservationUtcNs: maximumDecimal,
595
+ lastObservationUtcNs: maximumDecimal,
596
+ observations: [],
597
+ capturedCount64: maximumDecimal,
598
+ droppedBeforeBatch64: maximumDecimal,
599
+ compression: "gzip-json"
600
+ };
601
+ return Buffer.byteLength(JSON.stringify(value), "utf8");
602
+ }
603
+ function jsonUtf8SizeUpperBound(value) {
604
+ if (value === null || value === undefined)
605
+ return 4;
606
+ if (typeof value === "string")
607
+ return 2 + escapedJsonStringUtf8Bytes(value);
608
+ if (typeof value === "boolean")
609
+ return value ? 4 : 5;
610
+ if (typeof value === "number")
611
+ return Number.isFinite(value) ? Buffer.byteLength(String(value), "utf8") : 4;
612
+ if (typeof value === "bigint")
613
+ return Buffer.byteLength(value.toString(), "utf8");
614
+ if (Array.isArray(value)) {
615
+ return 2 + value.reduce((total, item, index) => total + (index ? 1 : 0) + jsonUtf8SizeUpperBound(item), 0);
616
+ }
617
+ if (typeof value === "object") {
618
+ let total = 2;
619
+ let count = 0;
620
+ for (const [key, item] of Object.entries(value)) {
621
+ if (item === undefined || typeof item === "function" || typeof item === "symbol")
622
+ continue;
623
+ total += (count ? 1 : 0) + 2 + escapedJsonStringUtf8Bytes(key) + 1 + jsonUtf8SizeUpperBound(item);
624
+ count += 1;
625
+ }
626
+ return total;
627
+ }
628
+ return 4;
629
+ }
630
+ function escapedJsonStringUtf8Bytes(value) {
631
+ let bytes = 0;
632
+ for (let index = 0; index < value.length; index += 1) {
633
+ const code = value.charCodeAt(index);
634
+ if (code <= 0x1f)
635
+ bytes += 6;
636
+ else if (code === 0x22 || code === 0x5c)
637
+ bytes += 2;
638
+ else if (code <= 0x7f)
639
+ bytes += 1;
640
+ else if (code <= 0x7ff)
641
+ bytes += 2;
642
+ else if (code >= 0xd800 && code <= 0xdbff) {
643
+ const next = value.charCodeAt(index + 1);
644
+ if (next >= 0xdc00 && next <= 0xdfff) {
645
+ bytes += 4;
646
+ index += 1;
647
+ }
648
+ else
649
+ bytes += 6;
650
+ }
651
+ else if (code >= 0xdc00 && code <= 0xdfff)
652
+ bytes += 6;
653
+ else
654
+ bytes += 3;
655
+ }
656
+ return bytes;
657
+ }
658
+ function derivePrefixedSha256(prefix, material) {
659
+ return `${prefix}${(0, node_crypto_1.createHash)("sha256").update(material, "utf8").digest("hex").slice(0, 40)}`;
660
+ }
661
+ function decimalString(value) {
662
+ try {
663
+ return BigInt(String(value)).toString();
664
+ }
665
+ catch {
666
+ return "0";
667
+ }
668
+ }
669
+ function nonNegativeDecimalString(value) {
670
+ try {
671
+ const parsed = BigInt(String(value));
672
+ return (parsed < 0n ? 0n : parsed).toString();
673
+ }
674
+ catch {
675
+ return "0";
676
+ }
677
+ }
678
+ function positiveCanonicalDecimalString(value, label) {
679
+ if (typeof value === "number" && (!Number.isSafeInteger(value) || value < 1)) {
680
+ throw new RangeError(`${label} must be a canonical unsigned decimal greater than zero.`);
681
+ }
682
+ const text = String(value);
683
+ if (!/^[1-9][0-9]*$/.test(text)) {
684
+ throw new RangeError(`${label} must be a canonical unsigned decimal greater than zero.`);
685
+ }
686
+ return text;
687
+ }
688
+ function truncateUtf8(value, maxBytes) {
689
+ if (Buffer.byteLength(value, "utf8") <= maxBytes) {
690
+ return value;
691
+ }
692
+ let output = "";
693
+ for (const token of value) {
694
+ if (Buffer.byteLength(output + token, "utf8") > maxBytes) {
695
+ break;
696
+ }
697
+ output += token;
698
+ }
699
+ return output;
700
+ }
701
+ function deepFreezeObservation(observation) {
702
+ for (const step of observation.steps) {
703
+ Object.freeze(step);
704
+ }
705
+ Object.freeze(observation.steps);
706
+ return Object.freeze(observation);
707
+ }
708
+ function deepFreezeBatch(batch) {
709
+ Object.freeze(batch.observations);
710
+ return Object.freeze(batch);
711
+ }