@loadstrike/loadstrike-sdk 1.0.30401 → 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 +6 -0
- package/dist/cjs/iteration-observation-diagnostics.js +513 -0
- package/dist/cjs/iteration-observations.js +195 -22
- package/dist/cjs/local-report-input.js +21 -0
- package/dist/cjs/local.js +55 -10
- package/dist/cjs/report-history.js +421 -0
- package/dist/cjs/reporting-svg.js +116 -0
- package/dist/cjs/reporting.js +436 -140
- package/dist/cjs/runtime.js +313 -85
- package/dist/cjs/sink-retry-policy.js +52 -0
- package/dist/cjs/sinks.js +112 -9
- package/dist/cjs/transports.js +78 -25
- package/dist/esm/iteration-observation-diagnostics.js +508 -0
- package/dist/esm/iteration-observations.js +195 -22
- package/dist/esm/local-report-input.js +17 -0
- package/dist/esm/local.js +55 -10
- package/dist/esm/report-history.js +413 -0
- package/dist/esm/reporting-svg.js +113 -0
- package/dist/esm/reporting.js +436 -140
- package/dist/esm/runtime.js +313 -85
- package/dist/esm/sink-retry-policy.js +44 -0
- package/dist/esm/sinks.js +112 -9
- package/dist/esm/transports.js +78 -25
- package/dist/types/iteration-observation-diagnostics.d.ts +21 -0
- package/dist/types/iteration-observations.d.ts +5 -0
- 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 +25 -0
- package/dist/types/sink-retry-policy.d.ts +9 -0
- package/dist/types/sinks.d.ts +6 -0
- package/dist/types/transports.d.ts +4 -0
- package/package.json +2 -2
|
@@ -10,7 +10,10 @@ exports.createIterationObservation = createIterationObservation;
|
|
|
10
10
|
exports.serializeIterationObservationBatchGzipJson = serializeIterationObservationBatchGzipJson;
|
|
11
11
|
exports.validateIterationObservationSettings = validateIterationObservationSettings;
|
|
12
12
|
const node_crypto_1 = require("node:crypto");
|
|
13
|
+
const node_perf_hooks_1 = require("node:perf_hooks");
|
|
13
14
|
const node_zlib_1 = require("node:zlib");
|
|
15
|
+
const iteration_observation_diagnostics_js_1 = require("./iteration-observation-diagnostics.js");
|
|
16
|
+
const sink_retry_policy_js_1 = require("./sink-retry-policy.js");
|
|
14
17
|
exports.ITERATION_OBSERVATION_SCHEMA_VERSION = "loadstrike.iteration-observation/1";
|
|
15
18
|
exports.ITERATION_OBSERVATION_BATCH_SCHEMA_VERSION = "loadstrike.iteration-batch/1";
|
|
16
19
|
exports.ITERATION_OBSERVATION_STREAM_COMPLETION_SCHEMA_VERSION = "loadstrike.iteration-stream-completion/1";
|
|
@@ -153,10 +156,15 @@ class ProcessIterationObservationBuffer {
|
|
|
153
156
|
exports.ProcessIterationObservationBuffer = ProcessIterationObservationBuffer;
|
|
154
157
|
const processIterationObservationBuffer = new ProcessIterationObservationBuffer();
|
|
155
158
|
class SinkDispatcher {
|
|
156
|
-
constructor(target, depth, parallelism, onDropped) {
|
|
159
|
+
constructor(target, depth, parallelism, retryCount, retryBackoffMs, logger, runId, resultOwnerId, onDropped) {
|
|
157
160
|
this.target = target;
|
|
158
161
|
this.depth = depth;
|
|
159
162
|
this.parallelism = parallelism;
|
|
163
|
+
this.retryCount = retryCount;
|
|
164
|
+
this.retryBackoffMs = retryBackoffMs;
|
|
165
|
+
this.logger = logger;
|
|
166
|
+
this.runId = runId;
|
|
167
|
+
this.resultOwnerId = resultOwnerId;
|
|
160
168
|
this.onDropped = onDropped;
|
|
161
169
|
this.queue = [];
|
|
162
170
|
this.active = new Set();
|
|
@@ -164,6 +172,8 @@ class SinkDispatcher {
|
|
|
164
172
|
this.timedOut = false;
|
|
165
173
|
this.delivered = 0n;
|
|
166
174
|
this.dropped = 0n;
|
|
175
|
+
this.drainDeadlineMs = null;
|
|
176
|
+
this.pendingRetryDelays = new Set();
|
|
167
177
|
}
|
|
168
178
|
get deliveredCount() {
|
|
169
179
|
return this.delivered;
|
|
@@ -172,7 +182,7 @@ class SinkDispatcher {
|
|
|
172
182
|
return this.dropped;
|
|
173
183
|
}
|
|
174
184
|
get canPublishCompletion() {
|
|
175
|
-
return this.queue.length === 0 && this.active.size === 0;
|
|
185
|
+
return !this.timedOut && this.queue.length === 0 && this.active.size === 0;
|
|
176
186
|
}
|
|
177
187
|
enqueue(batch) {
|
|
178
188
|
if (this.timedOut || this.queue.length >= this.depth) {
|
|
@@ -200,7 +210,6 @@ class SinkDispatcher {
|
|
|
200
210
|
});
|
|
201
211
|
const timeout = new Promise((resolve) => {
|
|
202
212
|
timer = setTimeout(() => resolve(false), Math.max(timeoutMs, 1));
|
|
203
|
-
timer.unref?.();
|
|
204
213
|
});
|
|
205
214
|
const completed = await Promise.race([idle, timeout]);
|
|
206
215
|
if (timer) {
|
|
@@ -213,16 +222,30 @@ class SinkDispatcher {
|
|
|
213
222
|
}
|
|
214
223
|
return completed;
|
|
215
224
|
}
|
|
225
|
+
beginDrain(deadlineMs) {
|
|
226
|
+
this.drainDeadlineMs = deadlineMs;
|
|
227
|
+
const delaysThatCannotFit = Array.from(this.pendingRetryDelays)
|
|
228
|
+
.filter((pending) => pending.dueAtMs >= deadlineMs);
|
|
229
|
+
if (delaysThatCannotFit.length > 0) {
|
|
230
|
+
for (const pending of delaysThatCannotFit) {
|
|
231
|
+
pending.cancel();
|
|
232
|
+
}
|
|
233
|
+
this.timeoutOutstanding();
|
|
234
|
+
}
|
|
235
|
+
}
|
|
216
236
|
pump() {
|
|
217
237
|
while (!this.timedOut && this.active.size < this.parallelism && this.queue.length > 0) {
|
|
218
238
|
const item = this.queue.shift();
|
|
219
239
|
this.active.add(item);
|
|
220
240
|
Promise.resolve()
|
|
221
|
-
.then(() => this.
|
|
222
|
-
.then(() => {
|
|
223
|
-
if (!this.timedOut) {
|
|
241
|
+
.then(() => this.deliver(item))
|
|
242
|
+
.then((outcome) => {
|
|
243
|
+
if (outcome === "success" && !this.timedOut) {
|
|
224
244
|
this.delivered += item.count;
|
|
225
245
|
}
|
|
246
|
+
else if (outcome === "exhausted" && !this.timedOut) {
|
|
247
|
+
this.drop("observation_sink_delivery_failed", item.batch.observations);
|
|
248
|
+
}
|
|
226
249
|
})
|
|
227
250
|
.catch(() => {
|
|
228
251
|
if (!this.timedOut) {
|
|
@@ -237,10 +260,95 @@ class SinkDispatcher {
|
|
|
237
260
|
}
|
|
238
261
|
this.notifyIdle();
|
|
239
262
|
}
|
|
263
|
+
async deliver(item) {
|
|
264
|
+
const maximumAttempts = this.retryCount + 1;
|
|
265
|
+
for (let attempt = 1; attempt <= maximumAttempts; attempt += 1) {
|
|
266
|
+
if (this.timedOut || this.deadlineExpired()) {
|
|
267
|
+
return "deadline";
|
|
268
|
+
}
|
|
269
|
+
try {
|
|
270
|
+
await this.target.saveIterationBatch(item.batch);
|
|
271
|
+
if (attempt > 1) {
|
|
272
|
+
(0, iteration_observation_diagnostics_js_1.logIterationObservationRecovery)(this.logger, {
|
|
273
|
+
...this.diagnosticContext(item, attempt, maximumAttempts),
|
|
274
|
+
nextDelayMs: 0
|
|
275
|
+
});
|
|
276
|
+
}
|
|
277
|
+
return "success";
|
|
278
|
+
}
|
|
279
|
+
catch (error) {
|
|
280
|
+
const exhausted = attempt >= maximumAttempts;
|
|
281
|
+
const nextDelayMs = exhausted
|
|
282
|
+
? 0
|
|
283
|
+
: (0, sink_retry_policy_js_1.sinkRetryDelayMs)(this.retryBackoffMs, attempt);
|
|
284
|
+
(0, iteration_observation_diagnostics_js_1.logIterationObservationFailure)(this.logger, exhausted ? "error" : "warn", {
|
|
285
|
+
...this.diagnosticContext(item, attempt, maximumAttempts),
|
|
286
|
+
nextDelayMs
|
|
287
|
+
}, error);
|
|
288
|
+
if (exhausted) {
|
|
289
|
+
return "exhausted";
|
|
290
|
+
}
|
|
291
|
+
if (!await this.waitForRetry(attempt)) {
|
|
292
|
+
this.timeoutOutstanding();
|
|
293
|
+
return "deadline";
|
|
294
|
+
}
|
|
295
|
+
}
|
|
296
|
+
}
|
|
297
|
+
return "exhausted";
|
|
298
|
+
}
|
|
299
|
+
diagnosticContext(item, attempt, maximumAttempts) {
|
|
300
|
+
return {
|
|
301
|
+
sinkName: this.target.name,
|
|
302
|
+
operation: "observation-batch",
|
|
303
|
+
runId: this.runId,
|
|
304
|
+
resultOwnerId: this.resultOwnerId,
|
|
305
|
+
batchId: item.batch.batchId,
|
|
306
|
+
batchSequence64: item.batch.batchSequence64,
|
|
307
|
+
observationCount: Number(item.count),
|
|
308
|
+
attempt,
|
|
309
|
+
maximumAttempts
|
|
310
|
+
};
|
|
311
|
+
}
|
|
312
|
+
async waitForRetry(retryNumber) {
|
|
313
|
+
const delayMs = (0, sink_retry_policy_js_1.sinkRetryDelayMs)(this.retryBackoffMs, retryNumber);
|
|
314
|
+
const deadline = this.drainDeadlineMs;
|
|
315
|
+
if (deadline !== null && !delayCanFit(delayMs, deadline)) {
|
|
316
|
+
return false;
|
|
317
|
+
}
|
|
318
|
+
if (delayMs === 0) {
|
|
319
|
+
await (0, sink_retry_policy_js_1.cooperativeSinkRetryYield)();
|
|
320
|
+
return !this.timedOut && !this.deadlineExpired();
|
|
321
|
+
}
|
|
322
|
+
return await new Promise((resolve) => {
|
|
323
|
+
let settled = false;
|
|
324
|
+
let pending;
|
|
325
|
+
const finish = (completed) => {
|
|
326
|
+
if (settled)
|
|
327
|
+
return;
|
|
328
|
+
settled = true;
|
|
329
|
+
clearTimeout(timer);
|
|
330
|
+
this.pendingRetryDelays.delete(pending);
|
|
331
|
+
resolve(completed && !this.timedOut && !this.deadlineExpired());
|
|
332
|
+
};
|
|
333
|
+
const timer = setTimeout(() => finish(true), delayMs);
|
|
334
|
+
const cancel = () => finish(false);
|
|
335
|
+
pending = {
|
|
336
|
+
dueAtMs: node_perf_hooks_1.performance.now() + delayMs,
|
|
337
|
+
cancel
|
|
338
|
+
};
|
|
339
|
+
this.pendingRetryDelays.add(pending);
|
|
340
|
+
});
|
|
341
|
+
}
|
|
342
|
+
deadlineExpired() {
|
|
343
|
+
return this.drainDeadlineMs !== null && node_perf_hooks_1.performance.now() >= this.drainDeadlineMs;
|
|
344
|
+
}
|
|
240
345
|
timeoutOutstanding() {
|
|
241
346
|
if (this.timedOut || this.isIdle())
|
|
242
347
|
return;
|
|
243
348
|
this.timedOut = true;
|
|
349
|
+
for (const pending of Array.from(this.pendingRetryDelays)) {
|
|
350
|
+
pending.cancel();
|
|
351
|
+
}
|
|
244
352
|
const queued = [...this.queue];
|
|
245
353
|
const active = [...this.active];
|
|
246
354
|
this.queue.length = 0;
|
|
@@ -312,7 +420,7 @@ class IterationObservationReporter {
|
|
|
312
420
|
this.recordWarning("sink_iteration_batches_unsupported", sink.name, "", -1, 1n, "The reporting sink does not support raw iteration batches.");
|
|
313
421
|
return false;
|
|
314
422
|
})
|
|
315
|
-
.map((target) => new SinkDispatcher(target, this.settings.sinkQueueDepth, this.settings.sinkParallelism, (code, sinkName, observations) => this.recordSinkDrop(code, sinkName, observations)));
|
|
423
|
+
.map((target) => new SinkDispatcher(target, this.settings.sinkQueueDepth, this.settings.sinkParallelism, (0, sink_retry_policy_js_1.normalizeSinkRetryCount)(options.sinkRetryCount), (0, sink_retry_policy_js_1.normalizeSinkRetryBackoffMs)(options.sinkRetryBackoffMs), options.logger, options.runId, options.resultOwnerId, (code, sinkName, observations) => this.recordSinkDrop(code, sinkName, observations)));
|
|
316
424
|
if (this.dispatchers.length > 0) {
|
|
317
425
|
this.buffer.register(this.streamKey, this.settings.maxBufferBytes);
|
|
318
426
|
}
|
|
@@ -368,11 +476,14 @@ class IterationObservationReporter {
|
|
|
368
476
|
this.sealed = true;
|
|
369
477
|
this.clock.clearInterval(this.timer);
|
|
370
478
|
this.flushNow();
|
|
371
|
-
const started =
|
|
479
|
+
const started = node_perf_hooks_1.performance.now();
|
|
372
480
|
const deadline = started + this.settings.drainTimeoutMs;
|
|
481
|
+
for (const dispatcher of this.dispatchers) {
|
|
482
|
+
dispatcher.beginDrain(deadline);
|
|
483
|
+
}
|
|
373
484
|
const drainResults = new Array(this.dispatchers.length).fill(false);
|
|
374
485
|
const completionResults = await Promise.all(this.dispatchers.map(async (dispatcher, index) => {
|
|
375
|
-
const drained = await dispatcher.waitForIdle(Math.max(deadline -
|
|
486
|
+
const drained = await dispatcher.waitForIdle(Math.max(deadline - node_perf_hooks_1.performance.now(), 1));
|
|
376
487
|
drainResults[index] = drained;
|
|
377
488
|
if (!dispatcher.canPublishCompletion) {
|
|
378
489
|
return false;
|
|
@@ -387,19 +498,7 @@ class IterationObservationReporter {
|
|
|
387
498
|
&& dispatcher.droppedCount === 0n
|
|
388
499
|
&& dispatcher.deliveredCount === this.captured;
|
|
389
500
|
const completion = this.createCompletion(dispatcher, sinkReportingComplete);
|
|
390
|
-
|
|
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
|
-
}
|
|
501
|
+
const outcome = await this.completeWithRetries(complete, completion, deadline, dispatcher.target.name);
|
|
403
502
|
if (outcome === "success") {
|
|
404
503
|
return true;
|
|
405
504
|
}
|
|
@@ -435,6 +534,76 @@ class IterationObservationReporter {
|
|
|
435
534
|
};
|
|
436
535
|
return { ...this.finalResult };
|
|
437
536
|
}
|
|
537
|
+
async completeWithRetries(complete, completion, deadline, sinkName) {
|
|
538
|
+
const retryCount = (0, sink_retry_policy_js_1.normalizeSinkRetryCount)(this.options.sinkRetryCount);
|
|
539
|
+
const backoffMs = (0, sink_retry_policy_js_1.normalizeSinkRetryBackoffMs)(this.options.sinkRetryBackoffMs);
|
|
540
|
+
const maximumAttempts = retryCount + 1;
|
|
541
|
+
for (let attempt = 1; attempt <= maximumAttempts; attempt += 1) {
|
|
542
|
+
const remainingMs = deadline - node_perf_hooks_1.performance.now();
|
|
543
|
+
if (remainingMs <= 0) {
|
|
544
|
+
return "timeout";
|
|
545
|
+
}
|
|
546
|
+
let timer;
|
|
547
|
+
const outcome = await Promise.race([
|
|
548
|
+
Promise.resolve()
|
|
549
|
+
.then(() => complete(completion))
|
|
550
|
+
.then(() => ({ kind: "success" }), (error) => ({ kind: "failed", error })),
|
|
551
|
+
new Promise((resolve) => {
|
|
552
|
+
timer = setTimeout(() => resolve({ kind: "timeout" }), Math.max(remainingMs, 1));
|
|
553
|
+
})
|
|
554
|
+
]);
|
|
555
|
+
if (timer) {
|
|
556
|
+
clearTimeout(timer);
|
|
557
|
+
}
|
|
558
|
+
if (outcome.kind === "success") {
|
|
559
|
+
if (attempt > 1) {
|
|
560
|
+
(0, iteration_observation_diagnostics_js_1.logIterationObservationRecovery)(this.options.logger, {
|
|
561
|
+
sinkName,
|
|
562
|
+
operation: "observation-stream-completion",
|
|
563
|
+
runId: this.options.runId,
|
|
564
|
+
resultOwnerId: this.options.resultOwnerId,
|
|
565
|
+
observationCount: 0,
|
|
566
|
+
attempt,
|
|
567
|
+
maximumAttempts,
|
|
568
|
+
nextDelayMs: 0
|
|
569
|
+
});
|
|
570
|
+
}
|
|
571
|
+
return "success";
|
|
572
|
+
}
|
|
573
|
+
if (outcome.kind === "timeout") {
|
|
574
|
+
return "timeout";
|
|
575
|
+
}
|
|
576
|
+
const exhausted = attempt >= maximumAttempts;
|
|
577
|
+
const nextDelayMs = exhausted ? 0 : (0, sink_retry_policy_js_1.sinkRetryDelayMs)(backoffMs, attempt);
|
|
578
|
+
(0, iteration_observation_diagnostics_js_1.logIterationObservationFailure)(this.options.logger, exhausted ? "error" : "warn", {
|
|
579
|
+
sinkName,
|
|
580
|
+
operation: "observation-stream-completion",
|
|
581
|
+
runId: this.options.runId,
|
|
582
|
+
resultOwnerId: this.options.resultOwnerId,
|
|
583
|
+
observationCount: 0,
|
|
584
|
+
attempt,
|
|
585
|
+
maximumAttempts,
|
|
586
|
+
nextDelayMs
|
|
587
|
+
}, outcome.error);
|
|
588
|
+
if (exhausted) {
|
|
589
|
+
return "failed";
|
|
590
|
+
}
|
|
591
|
+
const delayMs = nextDelayMs;
|
|
592
|
+
if (!delayCanFit(delayMs, deadline)) {
|
|
593
|
+
return "timeout";
|
|
594
|
+
}
|
|
595
|
+
if (delayMs === 0) {
|
|
596
|
+
await (0, sink_retry_policy_js_1.cooperativeSinkRetryYield)();
|
|
597
|
+
}
|
|
598
|
+
else {
|
|
599
|
+
await (0, sink_retry_policy_js_1.waitForSinkRetryDelay)(delayMs);
|
|
600
|
+
}
|
|
601
|
+
if (node_perf_hooks_1.performance.now() >= deadline) {
|
|
602
|
+
return "timeout";
|
|
603
|
+
}
|
|
604
|
+
}
|
|
605
|
+
return "failed";
|
|
606
|
+
}
|
|
438
607
|
buildWarnings() {
|
|
439
608
|
return Array.from(this.warnings.values())
|
|
440
609
|
.sort((left, right) => left.code.localeCompare(right.code)
|
|
@@ -698,6 +867,10 @@ function truncateUtf8(value, maxBytes) {
|
|
|
698
867
|
}
|
|
699
868
|
return output;
|
|
700
869
|
}
|
|
870
|
+
function delayCanFit(delayMs, deadlineMs) {
|
|
871
|
+
const remainingMs = deadlineMs - node_perf_hooks_1.performance.now();
|
|
872
|
+
return remainingMs > 0 && delayMs < remainingMs;
|
|
873
|
+
}
|
|
701
874
|
function deepFreezeObservation(observation) {
|
|
702
875
|
for (const step of observation.steps) {
|
|
703
876
|
Object.freeze(step);
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.emptyLocalReportInput = emptyLocalReportInput;
|
|
4
|
+
exports.distributedLocalReportInput = distributedLocalReportInput;
|
|
5
|
+
function emptyLocalReportInput() {
|
|
6
|
+
return {
|
|
7
|
+
history: {
|
|
8
|
+
status: "available",
|
|
9
|
+
points: []
|
|
10
|
+
}
|
|
11
|
+
};
|
|
12
|
+
}
|
|
13
|
+
function distributedLocalReportInput() {
|
|
14
|
+
return {
|
|
15
|
+
history: {
|
|
16
|
+
status: "unavailable",
|
|
17
|
+
reasonCategory: "distributed_temporal_aggregation_unavailable",
|
|
18
|
+
points: []
|
|
19
|
+
}
|
|
20
|
+
};
|
|
21
|
+
}
|
package/dist/cjs/local.js
CHANGED
|
@@ -1418,9 +1418,20 @@ function readTrackingId(payload, selector) {
|
|
|
1418
1418
|
return null;
|
|
1419
1419
|
}
|
|
1420
1420
|
let current = body;
|
|
1421
|
-
|
|
1421
|
+
let segments;
|
|
1422
|
+
try {
|
|
1423
|
+
segments = safeJsonPathSegments(path);
|
|
1424
|
+
}
|
|
1425
|
+
catch {
|
|
1426
|
+
return null;
|
|
1427
|
+
}
|
|
1428
|
+
for (const segment of segments) {
|
|
1422
1429
|
if (current && typeof current === "object" && !Array.isArray(current)) {
|
|
1423
|
-
|
|
1430
|
+
const record = current;
|
|
1431
|
+
if (!Object.prototype.hasOwnProperty.call(record, segment)) {
|
|
1432
|
+
return null;
|
|
1433
|
+
}
|
|
1434
|
+
current = record[segment];
|
|
1424
1435
|
}
|
|
1425
1436
|
else {
|
|
1426
1437
|
return null;
|
|
@@ -1453,23 +1464,57 @@ function readOptionalTrackingSelectorValue(value) {
|
|
|
1453
1464
|
return undefined;
|
|
1454
1465
|
}
|
|
1455
1466
|
function setJsonPathValue(body, path, value) {
|
|
1456
|
-
const target =
|
|
1457
|
-
|
|
1458
|
-
: {};
|
|
1459
|
-
const segments = path.split(".").filter(Boolean);
|
|
1467
|
+
const target = cloneJsonRecord(body);
|
|
1468
|
+
const segments = safeJsonPathSegments(path);
|
|
1460
1469
|
if (!segments.length) {
|
|
1461
1470
|
return target;
|
|
1462
1471
|
}
|
|
1463
1472
|
let current = target;
|
|
1464
1473
|
for (let i = 0; i < segments.length - 1; i += 1) {
|
|
1465
1474
|
const segment = segments[i];
|
|
1466
|
-
const next = current
|
|
1475
|
+
const next = readOwnJsonProperty(current, segment);
|
|
1476
|
+
let child;
|
|
1467
1477
|
if (!next || typeof next !== "object" || Array.isArray(next)) {
|
|
1468
|
-
|
|
1478
|
+
child = {};
|
|
1469
1479
|
}
|
|
1470
|
-
|
|
1480
|
+
else {
|
|
1481
|
+
child = cloneJsonRecord(next);
|
|
1482
|
+
}
|
|
1483
|
+
defineJsonProperty(current, segment, child);
|
|
1484
|
+
current = child;
|
|
1485
|
+
}
|
|
1486
|
+
defineJsonProperty(current, segments[segments.length - 1], value);
|
|
1487
|
+
return target;
|
|
1488
|
+
}
|
|
1489
|
+
const FORBIDDEN_JSON_PATH_SEGMENTS = new Set(["__proto__", "prototype", "constructor"]);
|
|
1490
|
+
function safeJsonPathSegments(path) {
|
|
1491
|
+
const segments = path.split(".").filter(Boolean);
|
|
1492
|
+
const forbidden = segments.find((segment) => FORBIDDEN_JSON_PATH_SEGMENTS.has(segment));
|
|
1493
|
+
if (forbidden) {
|
|
1494
|
+
throw new Error(`Tracking selector contains forbidden JSON path segment '${forbidden}'.`);
|
|
1495
|
+
}
|
|
1496
|
+
return segments;
|
|
1497
|
+
}
|
|
1498
|
+
function defineJsonProperty(target, key, value) {
|
|
1499
|
+
Object.defineProperty(target, key, {
|
|
1500
|
+
configurable: true,
|
|
1501
|
+
enumerable: true,
|
|
1502
|
+
value,
|
|
1503
|
+
writable: true
|
|
1504
|
+
});
|
|
1505
|
+
}
|
|
1506
|
+
function readOwnJsonProperty(target, key) {
|
|
1507
|
+
const descriptor = Object.getOwnPropertyDescriptor(target, key);
|
|
1508
|
+
return descriptor && "value" in descriptor ? descriptor.value : undefined;
|
|
1509
|
+
}
|
|
1510
|
+
function cloneJsonRecord(value) {
|
|
1511
|
+
const target = {};
|
|
1512
|
+
if (!value || typeof value !== "object" || Array.isArray(value)) {
|
|
1513
|
+
return target;
|
|
1514
|
+
}
|
|
1515
|
+
for (const [key, entry] of Object.entries(value)) {
|
|
1516
|
+
defineJsonProperty(target, key, entry);
|
|
1471
1517
|
}
|
|
1472
|
-
current[segments[segments.length - 1]] = value;
|
|
1473
1518
|
return target;
|
|
1474
1519
|
}
|
|
1475
1520
|
function mapCorrelationStore(tracking, runNamespace) {
|