@loadstrike/loadstrike-sdk 0.1.0

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,1009 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.CrossPlatformTrackingRuntime = exports.TrackingFieldSelector = exports.RedisCorrelationStore = exports.InMemoryCorrelationStore = exports.CorrelationStoreConfiguration = exports.RedisCorrelationStoreOptions = exports.TrackingPayloadBuilder = void 0;
4
+ const node_crypto_1 = require("node:crypto");
5
+ const redis_1 = require("redis");
6
+ class TrackingPayloadBuilder {
7
+ constructor() {
8
+ this.Headers = {};
9
+ this.body = new Uint8Array();
10
+ }
11
+ get Body() {
12
+ return new Uint8Array(this.body);
13
+ }
14
+ setBody(body) {
15
+ this.body = normalizeTrackingBodyToBytes(body);
16
+ }
17
+ SetBody(body) {
18
+ this.setBody(body);
19
+ }
20
+ build() {
21
+ return createTrackingPayload({
22
+ headers: { ...this.Headers },
23
+ body: new Uint8Array(this.body),
24
+ contentType: this.ContentType,
25
+ messagePayloadType: this.MessagePayloadType,
26
+ jsonSettings: cloneRecord(this.JsonSettings),
27
+ jsonConvertSettings: cloneRecord(this.JsonConvertSettings)
28
+ });
29
+ }
30
+ Build() {
31
+ return this.build();
32
+ }
33
+ }
34
+ exports.TrackingPayloadBuilder = TrackingPayloadBuilder;
35
+ class RedisCorrelationStoreOptions {
36
+ constructor() {
37
+ this.ConnectionString = "";
38
+ this.Database = -1;
39
+ this.KeyPrefix = "loadstrike";
40
+ this.EntryTtlSeconds = 60 * 60;
41
+ }
42
+ get EntryTtl() {
43
+ return this.EntryTtlSeconds;
44
+ }
45
+ set EntryTtl(value) {
46
+ this.EntryTtlSeconds = value;
47
+ }
48
+ validate() {
49
+ if (!this.ConnectionString.trim()) {
50
+ throw new Error("Redis ConnectionString must be provided.");
51
+ }
52
+ if (!this.KeyPrefix.trim()) {
53
+ throw new Error("Redis KeyPrefix must be provided.");
54
+ }
55
+ if (!Number.isFinite(this.EntryTtlSeconds) || this.EntryTtlSeconds <= 0) {
56
+ throw new RangeError("Redis EntryTtl must be greater than zero.");
57
+ }
58
+ }
59
+ Validate() {
60
+ this.validate();
61
+ }
62
+ }
63
+ exports.RedisCorrelationStoreOptions = RedisCorrelationStoreOptions;
64
+ class CorrelationStoreConfiguration {
65
+ constructor() {
66
+ this.Kind = "InMemory";
67
+ }
68
+ static inMemory() {
69
+ return new CorrelationStoreConfiguration();
70
+ }
71
+ static InMemory() {
72
+ return CorrelationStoreConfiguration.inMemory();
73
+ }
74
+ static redisStore(options) {
75
+ if (!(options instanceof RedisCorrelationStoreOptions)) {
76
+ throw new TypeError("Redis correlation store options must be provided.");
77
+ }
78
+ const configuration = new CorrelationStoreConfiguration();
79
+ configuration.Kind = "Redis";
80
+ configuration.Redis = options;
81
+ return configuration;
82
+ }
83
+ static RedisStore(options) {
84
+ return CorrelationStoreConfiguration.redisStore(options);
85
+ }
86
+ validate() {
87
+ if (this.Kind === "Redis") {
88
+ if (!(this.Redis instanceof RedisCorrelationStoreOptions)) {
89
+ throw new Error("Redis options must be provided for Redis correlation store.");
90
+ }
91
+ this.Redis.validate();
92
+ }
93
+ }
94
+ Validate() {
95
+ this.validate();
96
+ }
97
+ }
98
+ exports.CorrelationStoreConfiguration = CorrelationStoreConfiguration;
99
+ class InMemoryCorrelationStore {
100
+ constructor() {
101
+ this.pendingByTrackingId = new Map();
102
+ this.pendingByEventId = new Map();
103
+ this.sourceOccurrences = new Map();
104
+ this.destinationOccurrences = new Map();
105
+ }
106
+ set(entry) {
107
+ const normalized = normalizeCorrelationEntry(entry);
108
+ const queue = this.pendingByTrackingId.get(normalized.trackingId) ?? [];
109
+ queue.push(normalized);
110
+ this.pendingByTrackingId.set(normalized.trackingId, queue);
111
+ this.pendingByEventId.set(normalized.eventId ?? "", normalized);
112
+ }
113
+ get(trackingId) {
114
+ const queue = this.pendingByTrackingId.get(trackingId);
115
+ return queue && queue.length > 0 ? hydrateCorrelationEntry(queue[0]) : null;
116
+ }
117
+ delete(trackingId) {
118
+ const queue = this.pendingByTrackingId.get(trackingId);
119
+ if (!queue || queue.length === 0) {
120
+ return;
121
+ }
122
+ const removed = queue.shift();
123
+ if (removed?.eventId) {
124
+ this.pendingByEventId.delete(removed.eventId);
125
+ }
126
+ if (queue.length === 0) {
127
+ this.pendingByTrackingId.delete(trackingId);
128
+ return;
129
+ }
130
+ this.pendingByTrackingId.set(trackingId, queue);
131
+ }
132
+ all() {
133
+ const entries = [];
134
+ for (const queue of this.pendingByTrackingId.values()) {
135
+ for (const entry of queue) {
136
+ entries.push(hydrateCorrelationEntry(entry));
137
+ }
138
+ }
139
+ return entries;
140
+ }
141
+ collectExpired(nowMs = Date.now(), maxCount = 0) {
142
+ void nowMs;
143
+ void maxCount;
144
+ return [];
145
+ }
146
+ incrementSourceOccurrences(trackingId) {
147
+ const next = (this.sourceOccurrences.get(trackingId) ?? 0) + 1;
148
+ this.sourceOccurrences.set(trackingId, next);
149
+ return next;
150
+ }
151
+ incrementDestinationOccurrences(trackingId) {
152
+ const next = (this.destinationOccurrences.get(trackingId) ?? 0) + 1;
153
+ this.destinationOccurrences.set(trackingId, next);
154
+ return next;
155
+ }
156
+ registerSource(trackingId, sourceTimestampUtc) {
157
+ const entry = normalizeCorrelationEntry({
158
+ trackingId,
159
+ eventId: generateCorrelationEventId(trackingId, sourceTimestampUtc),
160
+ sourceTimestampUtc,
161
+ destinationTimestampUtc: null,
162
+ producedUtc: sourceTimestampUtc,
163
+ payload: createTrackingPayload({})
164
+ });
165
+ this.set(entry);
166
+ return entry;
167
+ }
168
+ tryMatchDestination(trackingId, destinationTimestampUtc) {
169
+ const source = this.get(trackingId);
170
+ if (!source) {
171
+ return null;
172
+ }
173
+ this.delete(trackingId);
174
+ return hydrateCorrelationEntry({
175
+ ...source,
176
+ destinationTimestampUtc
177
+ });
178
+ }
179
+ tryExpire(eventId) {
180
+ const entry = this.pendingByEventId.get(eventId);
181
+ if (!entry) {
182
+ return false;
183
+ }
184
+ this.pendingByEventId.delete(eventId);
185
+ const queue = this.pendingByTrackingId.get(entry.trackingId);
186
+ if (!queue) {
187
+ return true;
188
+ }
189
+ const next = queue.filter((value) => value.eventId !== eventId);
190
+ if (next.length === 0) {
191
+ this.pendingByTrackingId.delete(entry.trackingId);
192
+ }
193
+ else {
194
+ this.pendingByTrackingId.set(entry.trackingId, next);
195
+ }
196
+ return true;
197
+ }
198
+ }
199
+ exports.InMemoryCorrelationStore = InMemoryCorrelationStore;
200
+ class RedisCorrelationStore {
201
+ constructor(client, prefix = "loadstrike", options = {}) {
202
+ this.fallback = new InMemoryCorrelationStore();
203
+ this.fallbackExpiry = new Map();
204
+ this.client = client ?? null;
205
+ this.prefix = prefix;
206
+ this.pendingIndexKey = `${this.prefix}:pending:index`;
207
+ this.entryTtlMs = Math.max(Math.trunc(options.entryTtlMs ?? 3600000), 0);
208
+ this.now = options.now ?? (() => Date.now());
209
+ }
210
+ static fromOptions(options, runNamespace) {
211
+ options.validate();
212
+ const prefix = `${options.KeyPrefix}:${runNamespace}`;
213
+ return new RedisCorrelationStore(createRedisStoreClient(options.ConnectionString, options.Database), prefix, {
214
+ entryTtlMs: Math.max(Math.trunc(options.EntryTtlSeconds * 1000), 1)
215
+ });
216
+ }
217
+ async set(entry) {
218
+ const normalized = normalizeCorrelationEntry(entry);
219
+ if (!this.client?.set || !this.client.get || !this.client.del) {
220
+ this.fallback.set(normalized);
221
+ this.trackFallbackExpiry(normalized);
222
+ return;
223
+ }
224
+ await this.writeEntry(normalized);
225
+ await this.enqueuePendingId(normalized.trackingId, normalized.eventId ?? "");
226
+ await this.appendPendingIndexId(normalized.eventId ?? "");
227
+ }
228
+ async get(trackingId) {
229
+ if (!this.client?.set || !this.client.get || !this.client.del) {
230
+ while (true) {
231
+ const entry = this.fallback.get(trackingId);
232
+ if (!entry) {
233
+ return null;
234
+ }
235
+ if (!isEntryExpired(entry, this.fallbackExpiry.get(entry.eventId ?? ""), this.now())) {
236
+ return entry;
237
+ }
238
+ if (entry.eventId) {
239
+ this.fallback.tryExpire(entry.eventId);
240
+ this.fallbackExpiry.delete(entry.eventId);
241
+ continue;
242
+ }
243
+ this.fallback.delete(trackingId);
244
+ }
245
+ }
246
+ const queue = await this.readQueue(trackingId);
247
+ while (queue.length > 0) {
248
+ const eventId = queue[0];
249
+ const entry = await this.readEntry(eventId);
250
+ if (!entry) {
251
+ queue.shift();
252
+ await this.writeQueue(trackingId, queue);
253
+ await this.removePendingIndexId(eventId);
254
+ continue;
255
+ }
256
+ if (isEntryExpired(entry, entry.expiresUtc, this.now())) {
257
+ await this.tryExpire(eventId);
258
+ continue;
259
+ }
260
+ return hydrateCorrelationEntry(entry);
261
+ }
262
+ return null;
263
+ }
264
+ async delete(trackingId) {
265
+ if (!this.client?.set || !this.client.get || !this.client.del) {
266
+ const existing = this.fallback.get(trackingId);
267
+ this.fallback.delete(trackingId);
268
+ if (existing?.eventId) {
269
+ this.fallbackExpiry.delete(existing.eventId);
270
+ }
271
+ return;
272
+ }
273
+ const queue = await this.readQueue(trackingId);
274
+ const eventId = queue.shift();
275
+ if (!eventId) {
276
+ return;
277
+ }
278
+ await this.writeQueue(trackingId, queue);
279
+ await this.deleteEventKey(eventId);
280
+ await this.removePendingIndexId(eventId);
281
+ }
282
+ async all() {
283
+ if (!this.client?.set || !this.client.get || !this.client.del) {
284
+ await this.collectExpired(this.now());
285
+ return this.fallback.all();
286
+ }
287
+ const pendingIds = await this.readPendingIndexIds();
288
+ const entries = [];
289
+ const activeIds = [];
290
+ for (const eventId of pendingIds) {
291
+ const entry = await this.readEntry(eventId);
292
+ if (!entry) {
293
+ continue;
294
+ }
295
+ if (isEntryExpired(entry, entry.expiresUtc, this.now())) {
296
+ await this.tryExpire(eventId);
297
+ continue;
298
+ }
299
+ entries.push(hydrateCorrelationEntry(entry));
300
+ activeIds.push(eventId);
301
+ }
302
+ if (activeIds.length !== pendingIds.length) {
303
+ await this.writePendingIndexIds(activeIds);
304
+ }
305
+ return entries;
306
+ }
307
+ async collectExpired(nowMs = this.now(), maxCount = 0) {
308
+ const limit = maxCount > 0 ? Math.trunc(maxCount) : Number.POSITIVE_INFINITY;
309
+ const expired = [];
310
+ if (limit <= 0) {
311
+ return expired;
312
+ }
313
+ if (!this.client?.set || !this.client.get || !this.client.del) {
314
+ for (const entry of this.fallback.all()) {
315
+ if (expired.length >= limit) {
316
+ break;
317
+ }
318
+ const expiresUtc = this.fallbackExpiry.get(entry.eventId ?? "");
319
+ if (!isEntryExpired(entry, expiresUtc, nowMs)) {
320
+ continue;
321
+ }
322
+ if (entry.eventId && this.fallback.tryExpire(entry.eventId)) {
323
+ this.fallbackExpiry.delete(entry.eventId);
324
+ expired.push(entry);
325
+ }
326
+ }
327
+ return expired;
328
+ }
329
+ const pendingIds = await this.readPendingIndexIds();
330
+ const activeIds = [];
331
+ for (const eventId of pendingIds) {
332
+ if (expired.length >= limit) {
333
+ activeIds.push(eventId);
334
+ continue;
335
+ }
336
+ const entry = await this.readEntry(eventId);
337
+ if (!entry) {
338
+ continue;
339
+ }
340
+ if (!isEntryExpired(entry, entry.expiresUtc, nowMs)) {
341
+ activeIds.push(eventId);
342
+ continue;
343
+ }
344
+ const hydrated = hydrateCorrelationEntry(entry);
345
+ await this.tryExpire(eventId);
346
+ expired.push(hydrated);
347
+ }
348
+ if (activeIds.length !== pendingIds.length - expired.length) {
349
+ await this.writePendingIndexIds(activeIds);
350
+ }
351
+ return expired;
352
+ }
353
+ async incrementSourceOccurrences(trackingId) {
354
+ if (!this.client?.set || !this.client.get) {
355
+ return this.fallback.incrementSourceOccurrences(trackingId);
356
+ }
357
+ return this.incrementCounter(this.getSourceOccurrencesKey(trackingId));
358
+ }
359
+ async incrementDestinationOccurrences(trackingId) {
360
+ if (!this.client?.set || !this.client.get) {
361
+ return this.fallback.incrementDestinationOccurrences(trackingId);
362
+ }
363
+ return this.incrementCounter(this.getDestinationOccurrencesKey(trackingId));
364
+ }
365
+ async registerSource(trackingId, sourceTimestampUtc) {
366
+ const entry = normalizeCorrelationEntry({
367
+ trackingId,
368
+ eventId: generateCorrelationEventId(trackingId, sourceTimestampUtc),
369
+ sourceTimestampUtc,
370
+ destinationTimestampUtc: null,
371
+ producedUtc: sourceTimestampUtc,
372
+ payload: createTrackingPayload({})
373
+ });
374
+ await this.set(entry);
375
+ return entry;
376
+ }
377
+ async tryMatchDestination(trackingId, destinationTimestampUtc) {
378
+ if (!this.client?.set || !this.client.get || !this.client.del) {
379
+ const source = this.fallback.tryMatchDestination(trackingId, destinationTimestampUtc);
380
+ if (!source) {
381
+ return null;
382
+ }
383
+ if (source.eventId) {
384
+ this.fallbackExpiry.delete(source.eventId);
385
+ }
386
+ return hydrateCorrelationEntry(source);
387
+ }
388
+ const source = await this.get(trackingId);
389
+ if (!source) {
390
+ return null;
391
+ }
392
+ await this.delete(trackingId);
393
+ return hydrateCorrelationEntry({
394
+ ...source,
395
+ destinationTimestampUtc
396
+ });
397
+ }
398
+ async tryExpire(eventId) {
399
+ if (!this.client?.set || !this.client.get || !this.client.del) {
400
+ const expired = this.fallback.tryExpire(eventId);
401
+ if (expired) {
402
+ this.fallbackExpiry.delete(eventId);
403
+ }
404
+ return expired;
405
+ }
406
+ const entry = await this.readEntry(eventId);
407
+ if (!entry) {
408
+ return false;
409
+ }
410
+ await this.deleteEventKey(eventId);
411
+ await this.removePendingIndexId(eventId);
412
+ await this.removePendingQueueId(entry.trackingId, eventId);
413
+ return true;
414
+ }
415
+ async close() {
416
+ await this.client?.close?.();
417
+ }
418
+ trackFallbackExpiry(entry) {
419
+ if (!entry.eventId) {
420
+ return;
421
+ }
422
+ if (this.entryTtlMs > 0) {
423
+ this.fallbackExpiry.set(entry.eventId, this.now() + this.entryTtlMs);
424
+ return;
425
+ }
426
+ this.fallbackExpiry.delete(entry.eventId);
427
+ }
428
+ async incrementCounter(key) {
429
+ const raw = await this.client?.get?.(key);
430
+ const next = Number.parseInt(String(raw ?? "0"), 10) + 1;
431
+ await this.client?.set?.(key, String(next));
432
+ return next;
433
+ }
434
+ async readPendingIndexIds() {
435
+ const raw = await this.client?.get?.(this.pendingIndexKey);
436
+ if (!raw) {
437
+ return [];
438
+ }
439
+ return parseStringList(raw);
440
+ }
441
+ async writePendingIndexIds(ids) {
442
+ await this.client?.set?.(this.pendingIndexKey, JSON.stringify(uniqueStringList(ids)));
443
+ }
444
+ async appendPendingIndexId(eventId) {
445
+ const pending = await this.readPendingIndexIds();
446
+ pending.push(eventId);
447
+ await this.writePendingIndexIds(pending);
448
+ }
449
+ async removePendingIndexId(eventId) {
450
+ const pending = await this.readPendingIndexIds();
451
+ await this.writePendingIndexIds(pending.filter((value) => value !== eventId));
452
+ }
453
+ async readQueue(trackingId) {
454
+ const raw = await this.client?.get?.(this.getQueueKey(trackingId));
455
+ if (!raw) {
456
+ return [];
457
+ }
458
+ return parseStringList(raw);
459
+ }
460
+ async writeQueue(trackingId, ids) {
461
+ const queueKey = this.getQueueKey(trackingId);
462
+ if (ids.length === 0) {
463
+ await this.client?.del?.(queueKey);
464
+ return;
465
+ }
466
+ await this.client?.set?.(queueKey, JSON.stringify(ids));
467
+ }
468
+ async enqueuePendingId(trackingId, eventId) {
469
+ const queue = await this.readQueue(trackingId);
470
+ queue.push(eventId);
471
+ await this.writeQueue(trackingId, queue);
472
+ }
473
+ async removePendingQueueId(trackingId, eventId) {
474
+ const queue = await this.readQueue(trackingId);
475
+ await this.writeQueue(trackingId, queue.filter((value) => value !== eventId));
476
+ }
477
+ async readEntry(eventId) {
478
+ const raw = await this.client?.get?.(this.getEventKey(eventId));
479
+ if (!raw) {
480
+ return null;
481
+ }
482
+ try {
483
+ const parsed = JSON.parse(raw);
484
+ return normalizeCorrelationEntry({
485
+ trackingId: String(parsed.trackingId ?? ""),
486
+ eventId: String(parsed.eventId ?? eventId),
487
+ sourceTimestampUtc: Number(parsed.sourceTimestampUtc ?? parsed.producedUtc ?? 0),
488
+ destinationTimestampUtc: parsed.destinationTimestampUtc == null ? null : Number(parsed.destinationTimestampUtc),
489
+ producedUtc: Number(parsed.producedUtc ?? parsed.sourceTimestampUtc ?? 0),
490
+ payload: parsed.payload ?? {}
491
+ }, parsed.expiresUtc);
492
+ }
493
+ catch {
494
+ return null;
495
+ }
496
+ }
497
+ async writeEntry(entry) {
498
+ await this.client?.set?.(this.getEventKey(entry.eventId ?? ""), JSON.stringify({
499
+ ...entry,
500
+ expiresUtc: this.entryTtlMs > 0 ? this.now() + this.entryTtlMs : null
501
+ }));
502
+ }
503
+ async deleteEventKey(eventId) {
504
+ await this.client?.del?.(this.getEventKey(eventId));
505
+ }
506
+ getQueueKey(trackingId) {
507
+ return `${this.prefix}:pending:queue:${encodeURIComponent(trackingId)}`;
508
+ }
509
+ getEventKey(eventId) {
510
+ return `${this.prefix}:pending:event:${eventId}`;
511
+ }
512
+ getSourceOccurrencesKey(trackingId) {
513
+ return `${this.prefix}:occ:src:${encodeURIComponent(trackingId)}`;
514
+ }
515
+ getDestinationOccurrencesKey(trackingId) {
516
+ return `${this.prefix}:occ:dst:${encodeURIComponent(trackingId)}`;
517
+ }
518
+ }
519
+ exports.RedisCorrelationStore = RedisCorrelationStore;
520
+ class TrackingFieldSelector {
521
+ constructor(location, path) {
522
+ this.kind = normalizeTrackingFieldLocation(location);
523
+ this.path = String(path ?? "");
524
+ }
525
+ static parse(value) {
526
+ const normalized = (value ?? "").trim();
527
+ const [kindRaw, ...rest] = normalized.split(":");
528
+ const path = rest.join(":").trim();
529
+ if (!path) {
530
+ throw new Error("Tracking field expression is invalid. Use `header:<name>` or `json:$.<path>`.");
531
+ }
532
+ return new TrackingFieldSelector(kindRaw.trim(), path);
533
+ }
534
+ static Parse(value) {
535
+ return TrackingFieldSelector.parse(value);
536
+ }
537
+ static tryParse(value) {
538
+ try {
539
+ return {
540
+ success: true,
541
+ selector: TrackingFieldSelector.parse(value)
542
+ };
543
+ }
544
+ catch {
545
+ return {
546
+ success: false,
547
+ selector: null
548
+ };
549
+ }
550
+ }
551
+ static TryParse(value) {
552
+ return TrackingFieldSelector.tryParse(value);
553
+ }
554
+ get Location() {
555
+ return this.kind === "header" ? "Header" : "Json";
556
+ }
557
+ get Path() {
558
+ return this.path;
559
+ }
560
+ extract(payload) {
561
+ if (this.kind === "header") {
562
+ const headerName = this.path.toLowerCase();
563
+ const headers = payload.headers ?? {};
564
+ for (const [key, value] of Object.entries(headers)) {
565
+ const parsed = String(value ?? "").trim();
566
+ if (key.toLowerCase() === headerName && parsed) {
567
+ return parsed;
568
+ }
569
+ }
570
+ return null;
571
+ }
572
+ const body = normalizeJsonBody(payload.body);
573
+ if (!body) {
574
+ return null;
575
+ }
576
+ const jsonPath = this.path.startsWith("$.") ? this.path.slice(2) : this.path;
577
+ const segments = jsonPath.split(".").filter(Boolean);
578
+ let current = body;
579
+ for (const segment of segments) {
580
+ current = readJsonPathSegment(current, segment);
581
+ if (current === undefined) {
582
+ return null;
583
+ }
584
+ }
585
+ if (current == null) {
586
+ return null;
587
+ }
588
+ return String(current);
589
+ }
590
+ toString() {
591
+ return `${this.kind}:${this.path}`;
592
+ }
593
+ ToString() {
594
+ return this.toString();
595
+ }
596
+ }
597
+ exports.TrackingFieldSelector = TrackingFieldSelector;
598
+ function normalizeTrackingFieldLocation(location) {
599
+ const normalized = String(location ?? "").trim().toLowerCase();
600
+ if (normalized === "header" || normalized === "json") {
601
+ return normalized;
602
+ }
603
+ throw new Error("Tracking field location must be Header or Json.");
604
+ }
605
+ class CrossPlatformTrackingRuntime {
606
+ constructor(options) {
607
+ this.producedCount = 0;
608
+ this.consumedCount = 0;
609
+ this.matchedCount = 0;
610
+ this.timeoutCount = 0;
611
+ this.duplicateSourceCount = 0;
612
+ this.duplicateDestinationCount = 0;
613
+ this.totalLatencyMs = 0;
614
+ this.gathered = new Map();
615
+ this.sourceSelector = TrackingFieldSelector.parse(options.sourceTrackingField);
616
+ this.destinationSelector = TrackingFieldSelector.parse(options.destinationTrackingField ?? options.sourceTrackingField);
617
+ this.gatherSelector = options.destinationGatherByField
618
+ ? TrackingFieldSelector.parse(options.destinationGatherByField)
619
+ : null;
620
+ this.timeoutMs = Math.max(options.correlationTimeoutMs ?? 30000, 1);
621
+ this.timeoutCountsAsFailure = options.timeoutCountsAsFailure ?? true;
622
+ this.store = options.store ?? new InMemoryCorrelationStore();
623
+ this.plugins = options.plugins ?? [];
624
+ }
625
+ async onSourceProduced(payload, nowMs = Date.now()) {
626
+ const result = await this.onSourceProducedDetailed(payload, nowMs);
627
+ return result.trackingId || null;
628
+ }
629
+ async onSourceProducedDetailed(payload, nowMs = Date.now()) {
630
+ const result = {
631
+ trackingId: "",
632
+ eventId: "",
633
+ sourceTimestampUtc: nowMs,
634
+ duplicateSource: false
635
+ };
636
+ this.producedCount += 1;
637
+ const trackingId = this.sourceSelector.extract(payload);
638
+ if (!trackingId) {
639
+ return result;
640
+ }
641
+ result.trackingId = trackingId;
642
+ const sourceOccurrences = await this.store.incrementSourceOccurrences(trackingId);
643
+ if (sourceOccurrences > 1) {
644
+ this.duplicateSourceCount += 1;
645
+ result.duplicateSource = true;
646
+ }
647
+ const registration = await this.store.registerSource(trackingId, nowMs);
648
+ result.eventId = registration.eventId ?? "";
649
+ if (registration.eventId) {
650
+ await this.store.tryExpire(registration.eventId);
651
+ }
652
+ await this.store.set({
653
+ ...registration,
654
+ payload,
655
+ producedUtc: registration.producedUtc ?? nowMs,
656
+ sourceTimestampUtc: registration.sourceTimestampUtc ?? nowMs,
657
+ destinationTimestampUtc: null
658
+ });
659
+ for (const plugin of this.plugins) {
660
+ if (plugin.onProduced) {
661
+ await plugin.onProduced(trackingId, payload);
662
+ }
663
+ }
664
+ return result;
665
+ }
666
+ async onDestinationConsumed(payload, nowMs = Date.now()) {
667
+ const result = await this.onDestinationConsumedDetailed(payload, nowMs);
668
+ return result.matched;
669
+ }
670
+ async onDestinationConsumedDetailed(payload, nowMs = Date.now()) {
671
+ const result = {
672
+ trackingId: "",
673
+ eventId: "",
674
+ sourceTimestampUtc: 0,
675
+ destinationTimestampUtc: 0,
676
+ gatherKey: "__ungrouped__",
677
+ matched: false,
678
+ duplicateDestination: false,
679
+ latencyMs: 0,
680
+ message: ""
681
+ };
682
+ this.consumedCount += 1;
683
+ const trackingId = this.destinationSelector.extract(payload);
684
+ if (!trackingId) {
685
+ result.message = "Destination payload did not include a tracking id.";
686
+ return result;
687
+ }
688
+ result.trackingId = trackingId;
689
+ const destinationOccurrences = await this.store.incrementDestinationOccurrences(trackingId);
690
+ if (destinationOccurrences > 1) {
691
+ this.duplicateDestinationCount += 1;
692
+ result.duplicateDestination = true;
693
+ }
694
+ const source = await this.store.tryMatchDestination(trackingId, nowMs);
695
+ if (!source) {
696
+ result.message = "Destination tracking id had no source match.";
697
+ return result;
698
+ }
699
+ this.matchedCount += 1;
700
+ const latencyMs = Math.max(nowMs - (source.producedUtc ?? source.sourceTimestampUtc ?? nowMs), 0);
701
+ this.totalLatencyMs += latencyMs;
702
+ const gatherKey = this.resolveGatherKey(payload);
703
+ result.eventId = source.eventId ?? "";
704
+ result.sourceTimestampUtc = source.sourceTimestampUtc ?? source.producedUtc ?? 0;
705
+ result.destinationTimestampUtc = nowMs;
706
+ result.gatherKey = gatherKey;
707
+ result.matched = true;
708
+ result.latencyMs = latencyMs;
709
+ const row = this.gathered.get(gatherKey) ?? { total: 0, matched: 0, timedOut: 0 };
710
+ row.total += 1;
711
+ row.matched += 1;
712
+ this.gathered.set(gatherKey, row);
713
+ for (const plugin of this.plugins) {
714
+ if (plugin.onMatched) {
715
+ await plugin.onMatched(trackingId, source.payload, payload, latencyMs);
716
+ }
717
+ }
718
+ return result;
719
+ }
720
+ async sweepTimeouts(nowMs = Date.now(), batchSize) {
721
+ return (await this.sweepTimeoutEntries(nowMs, batchSize)).length;
722
+ }
723
+ async sweepTimeoutEntries(nowMs = Date.now(), batchSize) {
724
+ const maxBatch = Number.isFinite(batchSize ?? Number.NaN) && (batchSize ?? 0) > 0
725
+ ? Math.trunc(batchSize ?? 0)
726
+ : Number.POSITIVE_INFINITY;
727
+ const expiredEntries = [];
728
+ const seen = new Set();
729
+ const collected = await this.store.collectExpired(nowMs, Number.isFinite(maxBatch) ? maxBatch : 0);
730
+ for (const entry of collected) {
731
+ if (expiredEntries.length >= maxBatch) {
732
+ break;
733
+ }
734
+ const dedupeKey = entry.eventId?.trim() || `${entry.trackingId}:${entry.sourceTimestampUtc ?? entry.producedUtc}`;
735
+ if (!dedupeKey || seen.has(dedupeKey)) {
736
+ continue;
737
+ }
738
+ seen.add(dedupeKey);
739
+ expiredEntries.push(entry);
740
+ }
741
+ const entries = await this.store.all();
742
+ for (const entry of entries) {
743
+ if (expiredEntries.length >= maxBatch) {
744
+ break;
745
+ }
746
+ if (nowMs - entry.producedUtc < this.timeoutMs) {
747
+ continue;
748
+ }
749
+ if (entry.eventId && !(await this.store.tryExpire(entry.eventId))) {
750
+ await this.store.delete(entry.trackingId);
751
+ }
752
+ else if (!entry.eventId) {
753
+ await this.store.delete(entry.trackingId);
754
+ }
755
+ const dedupeKey = entry.eventId?.trim() || `${entry.trackingId}:${entry.sourceTimestampUtc ?? entry.producedUtc}`;
756
+ if (!dedupeKey || seen.has(dedupeKey)) {
757
+ continue;
758
+ }
759
+ seen.add(dedupeKey);
760
+ expiredEntries.push(entry);
761
+ }
762
+ for (const entry of expiredEntries) {
763
+ this.timeoutCount += 1;
764
+ const gatherKey = this.resolveGatherKey(entry.payload);
765
+ const row = this.gathered.get(gatherKey) ?? { total: 0, matched: 0, timedOut: 0 };
766
+ row.total += 1;
767
+ row.timedOut += 1;
768
+ this.gathered.set(gatherKey, row);
769
+ for (const plugin of this.plugins) {
770
+ if (plugin.onTimeout) {
771
+ await plugin.onTimeout(entry.trackingId, entry.payload);
772
+ }
773
+ }
774
+ }
775
+ return expiredEntries;
776
+ }
777
+ async getStats() {
778
+ const inflight = (await this.store.all()).length;
779
+ return {
780
+ producedCount: this.producedCount,
781
+ consumedCount: this.consumedCount,
782
+ matchedCount: this.matchedCount,
783
+ timeoutCount: this.timeoutCount,
784
+ duplicateSourceCount: this.duplicateSourceCount,
785
+ duplicateDestinationCount: this.duplicateDestinationCount,
786
+ inflightCount: inflight,
787
+ averageLatencyMs: this.matchedCount > 0 ? this.totalLatencyMs / this.matchedCount : 0,
788
+ gathered: Object.fromEntries(this.gathered.entries())
789
+ };
790
+ }
791
+ resolveGatherKey(payload) {
792
+ if (!this.gatherSelector) {
793
+ return "__ungrouped__";
794
+ }
795
+ return this.gatherSelector.extract(payload) || "__unknown__";
796
+ }
797
+ isTimeoutCountedAsFailure() {
798
+ return this.timeoutCountsAsFailure;
799
+ }
800
+ }
801
+ exports.CrossPlatformTrackingRuntime = CrossPlatformTrackingRuntime;
802
+ function normalizeJsonBody(body) {
803
+ if (body instanceof Uint8Array) {
804
+ return normalizeJsonBody(trackingBodyToUtf8(body));
805
+ }
806
+ if (body && typeof body === "object" && !Array.isArray(body)) {
807
+ return body;
808
+ }
809
+ if (typeof body !== "string" || !body.trim()) {
810
+ return null;
811
+ }
812
+ const direct = tryParseObject(body);
813
+ if (direct) {
814
+ return direct;
815
+ }
816
+ const relaxed = sanitizeLooseJson(body);
817
+ return tryParseObject(relaxed);
818
+ }
819
+ function normalizeTrackingBodyToBytes(body) {
820
+ return typeof body === "string"
821
+ ? new TextEncoder().encode(body)
822
+ : new Uint8Array(body);
823
+ }
824
+ function trackingBodyToUtf8(body) {
825
+ if (body instanceof Uint8Array) {
826
+ return new TextDecoder().decode(body);
827
+ }
828
+ if (typeof body === "string") {
829
+ return body;
830
+ }
831
+ if (body == null) {
832
+ return "";
833
+ }
834
+ try {
835
+ return JSON.stringify(body);
836
+ }
837
+ catch {
838
+ return String(body);
839
+ }
840
+ }
841
+ function createTrackingPayload(payload) {
842
+ return {
843
+ headers: { ...(payload.headers ?? {}) },
844
+ body: cloneTrackingBody(payload.body),
845
+ producedUtc: payload.producedUtc,
846
+ contentType: payload.contentType,
847
+ messagePayloadType: payload.messagePayloadType,
848
+ jsonSettings: cloneRecord(payload.jsonSettings),
849
+ jsonConvertSettings: cloneRecord(payload.jsonConvertSettings),
850
+ getBodyAsUtf8: () => trackingBodyToUtf8(payload.body)
851
+ };
852
+ }
853
+ function cloneTrackingBody(value) {
854
+ if (value instanceof Uint8Array) {
855
+ return new Uint8Array(value);
856
+ }
857
+ if (Array.isArray(value)) {
858
+ return value.map((entry) => cloneTrackingBody(entry));
859
+ }
860
+ if (value && typeof value === "object") {
861
+ return { ...value };
862
+ }
863
+ return value;
864
+ }
865
+ function cloneRecord(value) {
866
+ return value ? { ...value } : undefined;
867
+ }
868
+ function generateCorrelationEventId(trackingId, sourceTimestampUtc) {
869
+ return `${trackingId}:${sourceTimestampUtc}:${(0, node_crypto_1.randomUUID)()}`;
870
+ }
871
+ function normalizeCorrelationEntry(entry, expiresUtc) {
872
+ const trackingId = String(entry.trackingId ?? "").trim();
873
+ const producedUtc = Number(entry.producedUtc ?? entry.sourceTimestampUtc ?? 0);
874
+ const sourceTimestampUtc = Number(entry.sourceTimestampUtc ?? producedUtc);
875
+ const eventId = String(entry.eventId ?? generateCorrelationEventId(trackingId, sourceTimestampUtc));
876
+ return {
877
+ trackingId,
878
+ eventId,
879
+ sourceTimestampUtc,
880
+ destinationTimestampUtc: entry.destinationTimestampUtc == null ? null : Number(entry.destinationTimestampUtc),
881
+ producedUtc,
882
+ payload: createTrackingPayload(entry.payload ?? {}),
883
+ expiresUtc
884
+ };
885
+ }
886
+ function hydrateCorrelationEntry(entry) {
887
+ return {
888
+ trackingId: entry.trackingId,
889
+ eventId: entry.eventId,
890
+ sourceTimestampUtc: entry.sourceTimestampUtc,
891
+ destinationTimestampUtc: entry.destinationTimestampUtc ?? null,
892
+ producedUtc: entry.producedUtc,
893
+ payload: createTrackingPayload(entry.payload ?? {})
894
+ };
895
+ }
896
+ function isEntryExpired(entry, expiresUtc, nowMs) {
897
+ void entry;
898
+ return typeof expiresUtc === "number"
899
+ ? nowMs >= expiresUtc
900
+ : false;
901
+ }
902
+ function parseStringList(raw) {
903
+ try {
904
+ const parsed = JSON.parse(raw);
905
+ if (!Array.isArray(parsed)) {
906
+ return [];
907
+ }
908
+ return parsed
909
+ .map((value) => String(value ?? "").trim())
910
+ .filter((value) => value.length > 0);
911
+ }
912
+ catch {
913
+ return [];
914
+ }
915
+ }
916
+ function uniqueStringList(values) {
917
+ return [...new Set(values.filter((value) => value.trim().length > 0))];
918
+ }
919
+ function createRedisStoreClient(connectionString, database) {
920
+ const client = (0, redis_1.createClient)({
921
+ url: connectionString,
922
+ database: database >= 0 ? database : undefined
923
+ });
924
+ let connectPromise = null;
925
+ const ensureConnected = async () => {
926
+ if (client.isOpen) {
927
+ return client;
928
+ }
929
+ if (!connectPromise) {
930
+ connectPromise = client.connect().then(() => client);
931
+ }
932
+ return connectPromise;
933
+ };
934
+ return {
935
+ set: async (key, value) => {
936
+ const connected = await ensureConnected();
937
+ return connected.set(key, value);
938
+ },
939
+ get: async (key) => {
940
+ const connected = await ensureConnected();
941
+ return connected.get(key);
942
+ },
943
+ del: async (key) => {
944
+ const connected = await ensureConnected();
945
+ return connected.del(key);
946
+ },
947
+ keys: async (pattern) => {
948
+ const connected = await ensureConnected();
949
+ return connected.keys(pattern);
950
+ },
951
+ close: async () => {
952
+ if (client.isOpen) {
953
+ await client.quit();
954
+ }
955
+ }
956
+ };
957
+ }
958
+ function tryParseObject(value) {
959
+ try {
960
+ const parsed = JSON.parse(value);
961
+ if (parsed && typeof parsed === "object" && !Array.isArray(parsed)) {
962
+ return parsed;
963
+ }
964
+ }
965
+ catch {
966
+ return null;
967
+ }
968
+ return null;
969
+ }
970
+ function sanitizeLooseJson(value) {
971
+ let normalized = value.trim();
972
+ // Accept commonly seen non-strict payloads from external systems.
973
+ normalized = normalized.replace(/,\s*([}\]])/g, "$1");
974
+ normalized = normalized.replace(/'([^']*)'/g, (_match, group) => `"${group.replace(/"/g, '\\"')}"`);
975
+ return normalized;
976
+ }
977
+ function readJsonPathSegment(current, segment) {
978
+ if (current == null) {
979
+ return undefined;
980
+ }
981
+ const token = segment.trim();
982
+ const indexed = token.match(/^([^\[\]]+)\[(\d+)\]$/);
983
+ if (indexed) {
984
+ const field = indexed[1];
985
+ const index = Number(indexed[2]);
986
+ if (!Number.isFinite(index)) {
987
+ return undefined;
988
+ }
989
+ if (!(current && typeof current === "object" && !Array.isArray(current))) {
990
+ return undefined;
991
+ }
992
+ const row = current[field];
993
+ if (!Array.isArray(row) || index < 0 || index >= row.length) {
994
+ return undefined;
995
+ }
996
+ return row[index];
997
+ }
998
+ if (Array.isArray(current)) {
999
+ const arrayIndex = Number(token);
1000
+ if (!Number.isInteger(arrayIndex) || arrayIndex < 0 || arrayIndex >= current.length) {
1001
+ return undefined;
1002
+ }
1003
+ return current[arrayIndex];
1004
+ }
1005
+ if (typeof current === "object") {
1006
+ return current[token];
1007
+ }
1008
+ return undefined;
1009
+ }