@memberjunction/integration-engine 5.38.0 → 5.40.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.
Files changed (74) hide show
  1. package/dist/ActionMetadataGenerator.d.ts +8 -1
  2. package/dist/ActionMetadataGenerator.d.ts.map +1 -1
  3. package/dist/ActionMetadataGenerator.js +22 -3
  4. package/dist/ActionMetadataGenerator.js.map +1 -1
  5. package/dist/AdaptiveConcurrency.d.ts +85 -0
  6. package/dist/AdaptiveConcurrency.d.ts.map +1 -0
  7. package/dist/AdaptiveConcurrency.js +148 -0
  8. package/dist/AdaptiveConcurrency.js.map +1 -0
  9. package/dist/BaseIntegrationConnector.d.ts +127 -3
  10. package/dist/BaseIntegrationConnector.d.ts.map +1 -1
  11. package/dist/BaseIntegrationConnector.js +126 -11
  12. package/dist/BaseIntegrationConnector.js.map +1 -1
  13. package/dist/BaseRESTIntegrationConnector.d.ts +80 -15
  14. package/dist/BaseRESTIntegrationConnector.d.ts.map +1 -1
  15. package/dist/BaseRESTIntegrationConnector.js +314 -64
  16. package/dist/BaseRESTIntegrationConnector.js.map +1 -1
  17. package/dist/ConflictRecency.d.ts +24 -0
  18. package/dist/ConflictRecency.d.ts.map +1 -0
  19. package/dist/ConflictRecency.js +25 -0
  20. package/dist/ConflictRecency.js.map +1 -0
  21. package/dist/ContentHash.d.ts +28 -0
  22. package/dist/ContentHash.d.ts.map +1 -0
  23. package/dist/ContentHash.js +54 -0
  24. package/dist/ContentHash.js.map +1 -0
  25. package/dist/EnrichSchemaConstraints.d.ts +59 -0
  26. package/dist/EnrichSchemaConstraints.d.ts.map +1 -0
  27. package/dist/EnrichSchemaConstraints.js +168 -0
  28. package/dist/EnrichSchemaConstraints.js.map +1 -0
  29. package/dist/FieldMappingEngine.d.ts +22 -0
  30. package/dist/FieldMappingEngine.d.ts.map +1 -1
  31. package/dist/FieldMappingEngine.js +66 -5
  32. package/dist/FieldMappingEngine.js.map +1 -1
  33. package/dist/HashDiff.d.ts +68 -0
  34. package/dist/HashDiff.d.ts.map +1 -0
  35. package/dist/HashDiff.js +108 -0
  36. package/dist/HashDiff.js.map +1 -0
  37. package/dist/IntegrationActionGenerator.d.ts +93 -0
  38. package/dist/IntegrationActionGenerator.d.ts.map +1 -0
  39. package/dist/IntegrationActionGenerator.js +313 -0
  40. package/dist/IntegrationActionGenerator.js.map +1 -0
  41. package/dist/IntegrationConnectorCreationPipeline.d.ts +86 -0
  42. package/dist/IntegrationConnectorCreationPipeline.d.ts.map +1 -0
  43. package/dist/IntegrationConnectorCreationPipeline.js +226 -0
  44. package/dist/IntegrationConnectorCreationPipeline.js.map +1 -0
  45. package/dist/IntegrationEngine.d.ts +199 -1
  46. package/dist/IntegrationEngine.d.ts.map +1 -1
  47. package/dist/IntegrationEngine.js +1592 -109
  48. package/dist/IntegrationEngine.js.map +1 -1
  49. package/dist/IntegrationSchemaSync.d.ts +82 -0
  50. package/dist/IntegrationSchemaSync.d.ts.map +1 -1
  51. package/dist/IntegrationSchemaSync.js +289 -42
  52. package/dist/IntegrationSchemaSync.js.map +1 -1
  53. package/dist/MatchEngine.d.ts.map +1 -1
  54. package/dist/MatchEngine.js +4 -0
  55. package/dist/MatchEngine.js.map +1 -1
  56. package/dist/RateLimiter.d.ts +117 -0
  57. package/dist/RateLimiter.d.ts.map +1 -0
  58. package/dist/RateLimiter.js +159 -0
  59. package/dist/RateLimiter.js.map +1 -0
  60. package/dist/SyncLogger.d.ts +106 -0
  61. package/dist/SyncLogger.d.ts.map +1 -0
  62. package/dist/SyncLogger.js +176 -0
  63. package/dist/SyncLogger.js.map +1 -0
  64. package/dist/WatermarkService.d.ts +36 -1
  65. package/dist/WatermarkService.d.ts.map +1 -1
  66. package/dist/WatermarkService.js +113 -3
  67. package/dist/WatermarkService.js.map +1 -1
  68. package/dist/index.d.ts +23 -6
  69. package/dist/index.d.ts.map +1 -1
  70. package/dist/index.js +11 -1
  71. package/dist/index.js.map +1 -1
  72. package/dist/types.d.ts +52 -1
  73. package/dist/types.d.ts.map +1 -1
  74. package/package.json +8 -6
@@ -0,0 +1,117 @@
1
+ /**
2
+ * Adaptive, per-key token-bucket rate limiter (plan.md §7 — "peak-aware rate limiting").
3
+ *
4
+ * Each key (an IntegrationID) owns an independent token bucket that refills continuously
5
+ * at an *effective* rate and caps at a burst capacity. `Acquire(key)` waits until at
6
+ * least one token is available, then consumes it — letting the engine push a source to
7
+ * its real limits while never exceeding them.
8
+ *
9
+ * The "adaptive" part is classic AIMD (Additive-Increase / Multiplicative-Decrease), the
10
+ * same control law TCP congestion control uses:
11
+ * - `ReportThrottle(key, retryAfterMs?)` — the source signalled a 429 / rate-limit header.
12
+ * We MULTIPLICATIVELY cut the effective rate (×`ThrottleBackoffFactor`, default 0.5) and,
13
+ * if a Retry-After was given, FREEZE refills for that long so in-flight tokens drain.
14
+ * - `ReportSuccess(key)` — sustained success. We ADDITIVELY nudge the effective rate back up
15
+ * (+`SuccessRampPerCall`) but never past the configured ceiling (`TokensPerSec`). This finds
16
+ * the highest safe throughput and re-discovers it after the source recovers.
17
+ *
18
+ * Deterministic + testable: the clock (`now`) and the wait primitive (`sleep`) are injected
19
+ * via constructor options. Production defaults to wall-clock + a real `setTimeout`; tests pass
20
+ * fakes so NO real timers are involved and behaviour is fully reproducible.
21
+ *
22
+ * Pure: no DB, no network, no MJ metadata. The engine owns ONE instance and routes every
23
+ * outbound source call through `Acquire` / `ReportThrottle` / `ReportSuccess`.
24
+ */
25
+ /** Returns the current time in milliseconds (injectable for deterministic tests). */
26
+ export type NowFn = () => number;
27
+ /** Resolves after `ms` milliseconds (injectable for deterministic tests). */
28
+ export type SleepFn = (ms: number) => Promise<void>;
29
+ /** Construction options for {@link RateLimiter}. All fields optional; sensible defaults applied. */
30
+ export interface RateLimiterOptions {
31
+ /**
32
+ * Ceiling refill rate in tokens per second — the steady-state target a key ramps back
33
+ * toward and never exceeds. Also the rate a fresh key starts at. Default: 10.
34
+ */
35
+ TokensPerSec?: number;
36
+ /** Bucket capacity (max tokens that can accumulate) — the allowed burst size. Default: `TokensPerSec`. */
37
+ Burst?: number;
38
+ /**
39
+ * Multiplicative-decrease factor applied to the effective rate on each throttle signal
40
+ * (0 < f < 1). Default: 0.5 (halve the rate, like TCP).
41
+ */
42
+ ThrottleBackoffFactor?: number;
43
+ /**
44
+ * Additive-increase amount (tokens/sec) added to the effective rate on each success,
45
+ * clamped to `TokensPerSec`. Default: `TokensPerSec / 10` (≈10 successes to fully recover).
46
+ */
47
+ SuccessRampPerCall?: number;
48
+ /**
49
+ * Floor the effective rate can be cut to by repeated throttles, so a key never fully stalls
50
+ * (tokens/sec). Default: `TokensPerSec / 20`. Clamped to be > 0.
51
+ */
52
+ MinTokensPerSec?: number;
53
+ /** Time source. Default: `Date.now`. */
54
+ now?: NowFn;
55
+ /** Wait primitive. Default: a real `setTimeout`-based sleep. */
56
+ sleep?: SleepFn;
57
+ }
58
+ /** Read-only snapshot of a key's live limiter state — for tests, metrics, and debugging. */
59
+ export interface RateLimiterKeyState {
60
+ /** Current available tokens (fractional). */
61
+ Tokens: number;
62
+ /** Current effective refill rate in tokens/sec (between `MinTokensPerSec` and `TokensPerSec`). */
63
+ EffectiveTokensPerSec: number;
64
+ /** Bucket capacity (burst). */
65
+ Burst: number;
66
+ /** Epoch ms until which refills are frozen by a Retry-After (0 = not frozen). */
67
+ FrozenUntil: number;
68
+ }
69
+ /**
70
+ * Adaptive per-key token-bucket rate limiter. See file header for the full design.
71
+ * One instance per engine; keys are IntegrationIDs (or any stable string).
72
+ */
73
+ export declare class RateLimiter {
74
+ private readonly tokensPerSec;
75
+ private readonly burst;
76
+ private readonly throttleBackoffFactor;
77
+ private readonly successRampPerCall;
78
+ private readonly minTokensPerSec;
79
+ private readonly nowFn;
80
+ private readonly sleepFn;
81
+ private readonly buckets;
82
+ constructor(options?: RateLimiterOptions);
83
+ /**
84
+ * Waits until a token is available for `key`, then consumes exactly one. Returns the
85
+ * number of milliseconds spent waiting (0 if a token was immediately available) — useful
86
+ * for the engine to surface "we are being held back" telemetry.
87
+ */
88
+ Acquire(key: string): Promise<number>;
89
+ /**
90
+ * Multiplicative decrease: the source threw a 429 / rate-limit signal for `key`. Cuts the
91
+ * effective rate by `ThrottleBackoffFactor` (floored at `MinTokensPerSec`) and, when a
92
+ * Retry-After is supplied, freezes refills for that long so the bucket drains.
93
+ */
94
+ ReportThrottle(key: string, retryAfterMs?: number): void;
95
+ /**
96
+ * Additive increase: a call for `key` succeeded. Nudges the effective rate up by
97
+ * `SuccessRampPerCall`, never above the configured `TokensPerSec` ceiling (AIMD recovery).
98
+ */
99
+ ReportSuccess(key: string): void;
100
+ /** Returns a read-only snapshot of `key`'s state (refilled to "now"). Creates the bucket if absent. */
101
+ GetState(key: string): RateLimiterKeyState;
102
+ /** Drops all per-key state. Mainly for tests and engine teardown. */
103
+ Reset(): void;
104
+ /** Lazily creates a bucket starting full (one burst of tokens) at the ceiling rate. */
105
+ private getBucket;
106
+ /**
107
+ * Advances `bucket` to the current time: adds `effectiveRate × elapsedSeconds` tokens
108
+ * (capped at burst). While frozen by a Retry-After, no time accrues toward refill — the
109
+ * freeze interval is excluded so the bucket genuinely pauses, then resumes from the freeze end.
110
+ */
111
+ private refill;
112
+ /** Earliest time tokens may start accruing again, accounting for any active freeze window. */
113
+ private accrualStart;
114
+ /** Milliseconds until the next whole token, given the current (post-refill) deficit and freeze. */
115
+ private waitMsForToken;
116
+ }
117
+ //# sourceMappingURL=RateLimiter.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"RateLimiter.d.ts","sourceRoot":"","sources":["../src/RateLimiter.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;GAuBG;AAEH,qFAAqF;AACrF,MAAM,MAAM,KAAK,GAAG,MAAM,MAAM,CAAC;AAEjC,6EAA6E;AAC7E,MAAM,MAAM,OAAO,GAAG,CAAC,EAAE,EAAE,MAAM,KAAK,OAAO,CAAC,IAAI,CAAC,CAAC;AAEpD,oGAAoG;AACpG,MAAM,WAAW,kBAAkB;IAC/B;;;OAGG;IACH,YAAY,CAAC,EAAE,MAAM,CAAC;IACtB,0GAA0G;IAC1G,KAAK,CAAC,EAAE,MAAM,CAAC;IACf;;;OAGG;IACH,qBAAqB,CAAC,EAAE,MAAM,CAAC;IAC/B;;;OAGG;IACH,kBAAkB,CAAC,EAAE,MAAM,CAAC;IAC5B;;;OAGG;IACH,eAAe,CAAC,EAAE,MAAM,CAAC;IACzB,wCAAwC;IACxC,GAAG,CAAC,EAAE,KAAK,CAAC;IACZ,gEAAgE;IAChE,KAAK,CAAC,EAAE,OAAO,CAAC;CACnB;AAED,4FAA4F;AAC5F,MAAM,WAAW,mBAAmB;IAChC,6CAA6C;IAC7C,MAAM,EAAE,MAAM,CAAC;IACf,kGAAkG;IAClG,qBAAqB,EAAE,MAAM,CAAC;IAC9B,+BAA+B;IAC/B,KAAK,EAAE,MAAM,CAAC;IACd,iFAAiF;IACjF,WAAW,EAAE,MAAM,CAAC;CACvB;AAaD;;;GAGG;AACH,qBAAa,WAAW;IACpB,OAAO,CAAC,QAAQ,CAAC,YAAY,CAAS;IACtC,OAAO,CAAC,QAAQ,CAAC,KAAK,CAAS;IAC/B,OAAO,CAAC,QAAQ,CAAC,qBAAqB,CAAS;IAC/C,OAAO,CAAC,QAAQ,CAAC,kBAAkB,CAAS;IAC5C,OAAO,CAAC,QAAQ,CAAC,eAAe,CAAS;IACzC,OAAO,CAAC,QAAQ,CAAC,KAAK,CAAQ;IAC9B,OAAO,CAAC,QAAQ,CAAC,OAAO,CAAU;IAClC,OAAO,CAAC,QAAQ,CAAC,OAAO,CAA6B;gBAEzC,OAAO,GAAE,kBAAuB;IAU5C;;;;OAIG;IACU,OAAO,CAAC,GAAG,EAAE,MAAM,GAAG,OAAO,CAAC,MAAM,CAAC;IAelD;;;;OAIG;IACI,cAAc,CAAC,GAAG,EAAE,MAAM,EAAE,YAAY,CAAC,EAAE,MAAM,GAAG,IAAI;IAU/D;;;OAGG;IACI,aAAa,CAAC,GAAG,EAAE,MAAM,GAAG,IAAI;IAMvC,uGAAuG;IAChG,QAAQ,CAAC,GAAG,EAAE,MAAM,GAAG,mBAAmB;IAWjD,qEAAqE;IAC9D,KAAK,IAAI,IAAI;IAIpB,uFAAuF;IACvF,OAAO,CAAC,SAAS;IAcjB;;;;OAIG;IACH,OAAO,CAAC,MAAM;IAad,8FAA8F;IAC9F,OAAO,CAAC,YAAY;IAQpB,mGAAmG;IACnG,OAAO,CAAC,cAAc;CAOzB"}
@@ -0,0 +1,159 @@
1
+ /**
2
+ * Adaptive, per-key token-bucket rate limiter (plan.md §7 — "peak-aware rate limiting").
3
+ *
4
+ * Each key (an IntegrationID) owns an independent token bucket that refills continuously
5
+ * at an *effective* rate and caps at a burst capacity. `Acquire(key)` waits until at
6
+ * least one token is available, then consumes it — letting the engine push a source to
7
+ * its real limits while never exceeding them.
8
+ *
9
+ * The "adaptive" part is classic AIMD (Additive-Increase / Multiplicative-Decrease), the
10
+ * same control law TCP congestion control uses:
11
+ * - `ReportThrottle(key, retryAfterMs?)` — the source signalled a 429 / rate-limit header.
12
+ * We MULTIPLICATIVELY cut the effective rate (×`ThrottleBackoffFactor`, default 0.5) and,
13
+ * if a Retry-After was given, FREEZE refills for that long so in-flight tokens drain.
14
+ * - `ReportSuccess(key)` — sustained success. We ADDITIVELY nudge the effective rate back up
15
+ * (+`SuccessRampPerCall`) but never past the configured ceiling (`TokensPerSec`). This finds
16
+ * the highest safe throughput and re-discovers it after the source recovers.
17
+ *
18
+ * Deterministic + testable: the clock (`now`) and the wait primitive (`sleep`) are injected
19
+ * via constructor options. Production defaults to wall-clock + a real `setTimeout`; tests pass
20
+ * fakes so NO real timers are involved and behaviour is fully reproducible.
21
+ *
22
+ * Pure: no DB, no network, no MJ metadata. The engine owns ONE instance and routes every
23
+ * outbound source call through `Acquire` / `ReportThrottle` / `ReportSuccess`.
24
+ */
25
+ const DEFAULT_TOKENS_PER_SEC = 10;
26
+ const DEFAULT_BACKOFF_FACTOR = 0.5;
27
+ /**
28
+ * Adaptive per-key token-bucket rate limiter. See file header for the full design.
29
+ * One instance per engine; keys are IntegrationIDs (or any stable string).
30
+ */
31
+ export class RateLimiter {
32
+ constructor(options = {}) {
33
+ this.buckets = new Map();
34
+ this.tokensPerSec = positive(options.TokensPerSec, DEFAULT_TOKENS_PER_SEC);
35
+ this.burst = positive(options.Burst, this.tokensPerSec);
36
+ this.throttleBackoffFactor = clamp01Exclusive(options.ThrottleBackoffFactor, DEFAULT_BACKOFF_FACTOR);
37
+ this.successRampPerCall = positive(options.SuccessRampPerCall, this.tokensPerSec / 10);
38
+ this.minTokensPerSec = Math.min(positive(options.MinTokensPerSec, this.tokensPerSec / 20), this.tokensPerSec);
39
+ this.nowFn = options.now ?? (() => Date.now());
40
+ this.sleepFn = options.sleep ?? realSleep;
41
+ }
42
+ /**
43
+ * Waits until a token is available for `key`, then consumes exactly one. Returns the
44
+ * number of milliseconds spent waiting (0 if a token was immediately available) — useful
45
+ * for the engine to surface "we are being held back" telemetry.
46
+ */
47
+ async Acquire(key) {
48
+ const startMs = this.nowFn();
49
+ // Loop because the computed wait may land just before a refill tick (fractional rounding),
50
+ // or a concurrent Acquire on the same key may have taken the token we were waiting for.
51
+ for (;;) {
52
+ const bucket = this.getBucket(key);
53
+ this.refill(bucket);
54
+ if (bucket.tokens >= 1) {
55
+ bucket.tokens -= 1;
56
+ return this.nowFn() - startMs;
57
+ }
58
+ await this.sleepFn(this.waitMsForToken(bucket));
59
+ }
60
+ }
61
+ /**
62
+ * Multiplicative decrease: the source threw a 429 / rate-limit signal for `key`. Cuts the
63
+ * effective rate by `ThrottleBackoffFactor` (floored at `MinTokensPerSec`) and, when a
64
+ * Retry-After is supplied, freezes refills for that long so the bucket drains.
65
+ */
66
+ ReportThrottle(key, retryAfterMs) {
67
+ const bucket = this.getBucket(key);
68
+ this.refill(bucket);
69
+ bucket.effectiveRate = Math.max(this.minTokensPerSec, bucket.effectiveRate * this.throttleBackoffFactor);
70
+ if (retryAfterMs != null && retryAfterMs > 0) {
71
+ bucket.frozenUntilMs = this.nowFn() + retryAfterMs;
72
+ bucket.tokens = 0; // honour the source's "do not call until then"
73
+ }
74
+ }
75
+ /**
76
+ * Additive increase: a call for `key` succeeded. Nudges the effective rate up by
77
+ * `SuccessRampPerCall`, never above the configured `TokensPerSec` ceiling (AIMD recovery).
78
+ */
79
+ ReportSuccess(key) {
80
+ const bucket = this.getBucket(key);
81
+ this.refill(bucket);
82
+ bucket.effectiveRate = Math.min(this.tokensPerSec, bucket.effectiveRate + this.successRampPerCall);
83
+ }
84
+ /** Returns a read-only snapshot of `key`'s state (refilled to "now"). Creates the bucket if absent. */
85
+ GetState(key) {
86
+ const bucket = this.getBucket(key);
87
+ this.refill(bucket);
88
+ return {
89
+ Tokens: bucket.tokens,
90
+ EffectiveTokensPerSec: bucket.effectiveRate,
91
+ Burst: this.burst,
92
+ FrozenUntil: bucket.frozenUntilMs,
93
+ };
94
+ }
95
+ /** Drops all per-key state. Mainly for tests and engine teardown. */
96
+ Reset() {
97
+ this.buckets.clear();
98
+ }
99
+ /** Lazily creates a bucket starting full (one burst of tokens) at the ceiling rate. */
100
+ getBucket(key) {
101
+ let bucket = this.buckets.get(key);
102
+ if (!bucket) {
103
+ bucket = {
104
+ tokens: this.burst,
105
+ effectiveRate: this.tokensPerSec,
106
+ lastRefillMs: this.nowFn(),
107
+ frozenUntilMs: 0,
108
+ };
109
+ this.buckets.set(key, bucket);
110
+ }
111
+ return bucket;
112
+ }
113
+ /**
114
+ * Advances `bucket` to the current time: adds `effectiveRate × elapsedSeconds` tokens
115
+ * (capped at burst). While frozen by a Retry-After, no time accrues toward refill — the
116
+ * freeze interval is excluded so the bucket genuinely pauses, then resumes from the freeze end.
117
+ */
118
+ refill(bucket) {
119
+ const nowMs = this.nowFn();
120
+ if (nowMs <= bucket.lastRefillMs) {
121
+ return;
122
+ }
123
+ const accrualStartMs = this.accrualStart(bucket, nowMs);
124
+ if (nowMs > accrualStartMs) {
125
+ const elapsedSec = (nowMs - accrualStartMs) / 1000;
126
+ bucket.tokens = Math.min(this.burst, bucket.tokens + elapsedSec * bucket.effectiveRate);
127
+ }
128
+ bucket.lastRefillMs = nowMs;
129
+ }
130
+ /** Earliest time tokens may start accruing again, accounting for any active freeze window. */
131
+ accrualStart(bucket, nowMs) {
132
+ if (bucket.frozenUntilMs > bucket.lastRefillMs) {
133
+ // Some/all of the elapsed window was frozen; accrual only counts from the freeze end.
134
+ return Math.min(bucket.frozenUntilMs, nowMs);
135
+ }
136
+ return bucket.lastRefillMs;
137
+ }
138
+ /** Milliseconds until the next whole token, given the current (post-refill) deficit and freeze. */
139
+ waitMsForToken(bucket) {
140
+ const nowMs = this.nowFn();
141
+ const freezeRemainingMs = Math.max(0, bucket.frozenUntilMs - nowMs);
142
+ const deficit = 1 - bucket.tokens;
143
+ const refillMs = deficit > 0 ? Math.ceil((deficit / bucket.effectiveRate) * 1000) : 0;
144
+ return Math.max(1, freezeRemainingMs + refillMs);
145
+ }
146
+ }
147
+ /** Real wall-clock sleep — the only impure primitive, isolated here and overridden in tests. */
148
+ function realSleep(ms) {
149
+ return new Promise(resolve => setTimeout(resolve, ms));
150
+ }
151
+ /** Returns `value` if it is a finite positive number, else `fallback`. */
152
+ function positive(value, fallback) {
153
+ return value != null && Number.isFinite(value) && value > 0 ? value : fallback;
154
+ }
155
+ /** Returns `value` if it is a finite number strictly within (0, 1), else `fallback`. */
156
+ function clamp01Exclusive(value, fallback) {
157
+ return value != null && Number.isFinite(value) && value > 0 && value < 1 ? value : fallback;
158
+ }
159
+ //# sourceMappingURL=RateLimiter.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"RateLimiter.js","sourceRoot":"","sources":["../src/RateLimiter.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,106 @@
1
+ /**
2
+ * Structured logger for sync runs.
3
+ *
4
+ * Every meaningful step in `IntegrationEngine.RunSync()` emits ONE structured
5
+ * JSON line through this helper so operators tailing the MJAPI log file can
6
+ * grep + jq the event stream without parsing free-form prose.
7
+ *
8
+ * Vocabulary (the `event` field):
9
+ *
10
+ * sync.run.start — run dispatched; carries CompanyIntegration shape, config snapshot
11
+ * sync.config.loaded — entity-map + field-map counts and names available for the run
12
+ * sync.connector.built — connector instance constructed (class + import path)
13
+ * sync.connector.test — TestConnection result (success + duration)
14
+ * sync.entity-map.start — per-IO sync starting (direction, watermark, externalObjectName)
15
+ * sync.fetch.batch.start — outbound API fetch about to fire (page/offset/cursor + URL hints)
16
+ * sync.fetch.batch.complete — fetch returned (record count, duration, hasMore)
17
+ * sync.record.decision — per-record action (Create/Update/Skip/Delete) + reason + matchKey
18
+ * sync.record.saved — record persisted to MJ (action, mjEntity, mjID, durationMs)
19
+ * sync.record.error — record failed (classified code, MJ-side or external-side, action)
20
+ * sync.push.candidates — outbound push: count of changed records selected since last push
21
+ * sync.push.record — outbound push: about to send to external (externalID, body keys)
22
+ * sync.push.response — outbound push: response (status, externalIDReturned, durationMs)
23
+ * sync.entity-map.complete — per-IO stats rollup + new watermark
24
+ * sync.run.complete — run terminal: rollup + duration + status
25
+ * sync.run.fail — run terminal: error
26
+ * sync.run.cancelled — run terminal: abort signal fired
27
+ *
28
+ * Each line is `{ ts, event, ...data }` — `ts` is ISO 8601. All fields are
29
+ * consistently namespaced so a grep / jq pipeline can extract what it needs.
30
+ *
31
+ * To filter the log file:
32
+ * tail -f /tmp/mjapi.log | grep '"event":"sync\.' # all sync events
33
+ * tail -f /tmp/mjapi.log | grep '"event":"sync\.record\.' # per-record only
34
+ * tail -f /tmp/mjapi.log | grep '"event":"sync\.fetch\.' # only external fetches
35
+ * tail -f /tmp/mjapi.log | grep '"event":"sync\.push\.' # only outbound writes
36
+ * tail -f /tmp/mjapi.log | grep '"event":"sync\..*\.error"' # only errors
37
+ */
38
+ export type SyncLogEvent = 'sync.run.start' | 'sync.config.loaded' | 'sync.connector.built' | 'sync.connector.test' | 'sync.entity-map.start' | 'sync.resume.keyset' | 'sync.partition.reconcile' | 'sync.fetch.batch.start' | 'sync.fetch.batch.complete' | 'sync.record.decision' | 'sync.record.saved' | 'sync.record.error' | 'sync.record.archived' | 'sync.record.conflict' | 'sync.push.candidates' | 'sync.push.record' | 'sync.push.response' | 'sync.entity-map.complete' | 'sync.run.complete' | 'sync.run.fail' | 'sync.run.cancelled' | 'sync.warning';
39
+ import type { IntegrationProgressEmitter } from '@memberjunction/integration-progress-artifacts';
40
+ export interface SyncLoggerContext {
41
+ /** CompanyIntegration ID — the per-run anchor for filtering. */
42
+ ciId: string;
43
+ /** Integration name (HubSpot / YourMembership / …). */
44
+ integration: string | null | undefined;
45
+ /** Sync run ID for cross-correlation with run details rows. */
46
+ runId?: string | null;
47
+ }
48
+ export interface SyncLogEntry {
49
+ ts: string;
50
+ event: SyncLogEvent;
51
+ ciId: string;
52
+ integration?: string | null;
53
+ runId?: string | null;
54
+ /** Free-form, event-specific structured data. */
55
+ [key: string]: unknown;
56
+ }
57
+ /**
58
+ * Light wrapper that prepends an ISO timestamp + the per-run context to every
59
+ * line. Writes to console.log (or console.error for fail events) so the line
60
+ * lands in whatever stream the wrapper script is teeing to disk.
61
+ */
62
+ export declare class SyncLogger {
63
+ private readonly ctx;
64
+ private emitter?;
65
+ constructor(ctx: SyncLoggerContext);
66
+ /** Update the runId once the run record has been created. */
67
+ attachRunId(runId: string): void;
68
+ /** Set the integration name once it's resolved from LoadRunConfiguration. */
69
+ attachIntegrationName(name: string | null | undefined): void;
70
+ /**
71
+ * Attach a durable progress-artifact emitter. Once attached, the grep-friendly
72
+ * console events are ALSO forwarded (selectively, at stage/batch/error
73
+ * granularity — never per-record, to keep the stream queryable) to the run's
74
+ * append-only JSONL artifact so the sync is visible over GraphQL and survives an
75
+ * MJAPI restart. Terminal run.complete/run.fail are owned by the caller (which
76
+ * awaits the emitter's async terminal write), so they are NOT forwarded here.
77
+ */
78
+ attachEmitter(emitter: IntegrationProgressEmitter): void;
79
+ /**
80
+ * Persist a resumable CHECKPOINT (the sync position after a committed batch) into the durable
81
+ * progress artifact (plan.md §8a). On crash/restart the latest checkpoint's resumableState
82
+ * (watermark / keyset AfterKey / cursor / batchIndex) lets the run pick back up. Best-effort —
83
+ * no emitter attached → no-op.
84
+ */
85
+ checkpoint(stage: string, resumableState: Record<string, unknown>): void;
86
+ /**
87
+ * Emit a non-fatal STRUCTURED WARNING. Unlike record/fetch errors (which mark the run
88
+ * failed), a warning records a notable-but-non-fatal condition — the canonical case being a
89
+ * second-layer/association object that fetched zero records because its parents weren't
90
+ * available (the silent-empty). Forwarded to the durable artifact as a SyncWarning so the
91
+ * condition is visible over GraphQL instead of a swallowed console.warn, WITHOUT affecting
92
+ * run success. Goes to console.warn so it's also greppable in the tee'd log.
93
+ */
94
+ warning(stage: string, code: string, message: string, data?: Record<string, unknown>): void;
95
+ emit(event: SyncLogEvent, data?: Record<string, unknown>): void;
96
+ /**
97
+ * Best-effort mirror of a sync event into the durable artifact stream. Any
98
+ * failure here is swallowed — structured logging must never break a sync.
99
+ */
100
+ private forwardToEmitter;
101
+ private stageLabel;
102
+ private num;
103
+ /** Narrows an unknown to a plain record (or undefined) without a lazy `any` cast. */
104
+ private asRecord;
105
+ }
106
+ //# sourceMappingURL=SyncLogger.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"SyncLogger.d.ts","sourceRoot":"","sources":["../src/SyncLogger.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAoCG;AAEH,MAAM,MAAM,YAAY,GAClB,gBAAgB,GAChB,oBAAoB,GACpB,sBAAsB,GACtB,qBAAqB,GACrB,uBAAuB,GACvB,oBAAoB,GACpB,0BAA0B,GAC1B,wBAAwB,GACxB,2BAA2B,GAC3B,sBAAsB,GACtB,mBAAmB,GACnB,mBAAmB,GACnB,sBAAsB,GACtB,sBAAsB,GACtB,sBAAsB,GACtB,kBAAkB,GAClB,oBAAoB,GACpB,0BAA0B,GAC1B,mBAAmB,GACnB,eAAe,GACf,oBAAoB,GACpB,cAAc,CAAC;AAErB,OAAO,KAAK,EAAE,0BAA0B,EAAE,MAAM,gDAAgD,CAAC;AAEjG,MAAM,WAAW,iBAAiB;IAC9B,gEAAgE;IAChE,IAAI,EAAE,MAAM,CAAC;IACb,uDAAuD;IACvD,WAAW,EAAE,MAAM,GAAG,IAAI,GAAG,SAAS,CAAC;IACvC,+DAA+D;IAC/D,KAAK,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;CACzB;AAED,MAAM,WAAW,YAAY;IACzB,EAAE,EAAE,MAAM,CAAC;IACX,KAAK,EAAE,YAAY,CAAC;IACpB,IAAI,EAAE,MAAM,CAAC;IACb,WAAW,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;IAC5B,KAAK,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;IACtB,iDAAiD;IACjD,CAAC,GAAG,EAAE,MAAM,GAAG,OAAO,CAAC;CAC1B;AAED;;;;GAIG;AACH,qBAAa,UAAU;IACnB,OAAO,CAAC,QAAQ,CAAC,GAAG,CAAoB;IACxC,OAAO,CAAC,OAAO,CAAC,CAA6B;gBAEjC,GAAG,EAAE,iBAAiB;IAIlC,6DAA6D;IACtD,WAAW,CAAC,KAAK,EAAE,MAAM,GAAG,IAAI;IAIvC,6EAA6E;IACtE,qBAAqB,CAAC,IAAI,EAAE,MAAM,GAAG,IAAI,GAAG,SAAS,GAAG,IAAI;IAInE;;;;;;;OAOG;IACI,aAAa,CAAC,OAAO,EAAE,0BAA0B,GAAG,IAAI;IAI/D;;;;;OAKG;IACI,UAAU,CAAC,KAAK,EAAE,MAAM,EAAE,cAAc,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,GAAG,IAAI;IAI/E;;;;;;;OAOG;IACI,OAAO,CAAC,KAAK,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,EAAE,OAAO,EAAE,MAAM,EAAE,IAAI,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,GAAG,IAAI;IAI3F,IAAI,CAAC,KAAK,EAAE,YAAY,EAAE,IAAI,GAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAM,GAAG,IAAI;IAoB1E;;;OAGG;IACH,OAAO,CAAC,gBAAgB;IAyDxB,OAAO,CAAC,UAAU;IAKlB,OAAO,CAAC,GAAG;IAIX,qFAAqF;IACrF,OAAO,CAAC,QAAQ;CAGnB"}
@@ -0,0 +1,176 @@
1
+ /**
2
+ * Structured logger for sync runs.
3
+ *
4
+ * Every meaningful step in `IntegrationEngine.RunSync()` emits ONE structured
5
+ * JSON line through this helper so operators tailing the MJAPI log file can
6
+ * grep + jq the event stream without parsing free-form prose.
7
+ *
8
+ * Vocabulary (the `event` field):
9
+ *
10
+ * sync.run.start — run dispatched; carries CompanyIntegration shape, config snapshot
11
+ * sync.config.loaded — entity-map + field-map counts and names available for the run
12
+ * sync.connector.built — connector instance constructed (class + import path)
13
+ * sync.connector.test — TestConnection result (success + duration)
14
+ * sync.entity-map.start — per-IO sync starting (direction, watermark, externalObjectName)
15
+ * sync.fetch.batch.start — outbound API fetch about to fire (page/offset/cursor + URL hints)
16
+ * sync.fetch.batch.complete — fetch returned (record count, duration, hasMore)
17
+ * sync.record.decision — per-record action (Create/Update/Skip/Delete) + reason + matchKey
18
+ * sync.record.saved — record persisted to MJ (action, mjEntity, mjID, durationMs)
19
+ * sync.record.error — record failed (classified code, MJ-side or external-side, action)
20
+ * sync.push.candidates — outbound push: count of changed records selected since last push
21
+ * sync.push.record — outbound push: about to send to external (externalID, body keys)
22
+ * sync.push.response — outbound push: response (status, externalIDReturned, durationMs)
23
+ * sync.entity-map.complete — per-IO stats rollup + new watermark
24
+ * sync.run.complete — run terminal: rollup + duration + status
25
+ * sync.run.fail — run terminal: error
26
+ * sync.run.cancelled — run terminal: abort signal fired
27
+ *
28
+ * Each line is `{ ts, event, ...data }` — `ts` is ISO 8601. All fields are
29
+ * consistently namespaced so a grep / jq pipeline can extract what it needs.
30
+ *
31
+ * To filter the log file:
32
+ * tail -f /tmp/mjapi.log | grep '"event":"sync\.' # all sync events
33
+ * tail -f /tmp/mjapi.log | grep '"event":"sync\.record\.' # per-record only
34
+ * tail -f /tmp/mjapi.log | grep '"event":"sync\.fetch\.' # only external fetches
35
+ * tail -f /tmp/mjapi.log | grep '"event":"sync\.push\.' # only outbound writes
36
+ * tail -f /tmp/mjapi.log | grep '"event":"sync\..*\.error"' # only errors
37
+ */
38
+ /**
39
+ * Light wrapper that prepends an ISO timestamp + the per-run context to every
40
+ * line. Writes to console.log (or console.error for fail events) so the line
41
+ * lands in whatever stream the wrapper script is teeing to disk.
42
+ */
43
+ export class SyncLogger {
44
+ constructor(ctx) {
45
+ this.ctx = ctx;
46
+ }
47
+ /** Update the runId once the run record has been created. */
48
+ attachRunId(runId) {
49
+ this.ctx.runId = runId;
50
+ }
51
+ /** Set the integration name once it's resolved from LoadRunConfiguration. */
52
+ attachIntegrationName(name) {
53
+ this.ctx.integration = name;
54
+ }
55
+ /**
56
+ * Attach a durable progress-artifact emitter. Once attached, the grep-friendly
57
+ * console events are ALSO forwarded (selectively, at stage/batch/error
58
+ * granularity — never per-record, to keep the stream queryable) to the run's
59
+ * append-only JSONL artifact so the sync is visible over GraphQL and survives an
60
+ * MJAPI restart. Terminal run.complete/run.fail are owned by the caller (which
61
+ * awaits the emitter's async terminal write), so they are NOT forwarded here.
62
+ */
63
+ attachEmitter(emitter) {
64
+ this.emitter = emitter;
65
+ }
66
+ /**
67
+ * Persist a resumable CHECKPOINT (the sync position after a committed batch) into the durable
68
+ * progress artifact (plan.md §8a). On crash/restart the latest checkpoint's resumableState
69
+ * (watermark / keyset AfterKey / cursor / batchIndex) lets the run pick back up. Best-effort —
70
+ * no emitter attached → no-op.
71
+ */
72
+ checkpoint(stage, resumableState) {
73
+ this.emitter?.checkpoint(stage, resumableState);
74
+ }
75
+ /**
76
+ * Emit a non-fatal STRUCTURED WARNING. Unlike record/fetch errors (which mark the run
77
+ * failed), a warning records a notable-but-non-fatal condition — the canonical case being a
78
+ * second-layer/association object that fetched zero records because its parents weren't
79
+ * available (the silent-empty). Forwarded to the durable artifact as a SyncWarning so the
80
+ * condition is visible over GraphQL instead of a swallowed console.warn, WITHOUT affecting
81
+ * run success. Goes to console.warn so it's also greppable in the tee'd log.
82
+ */
83
+ warning(stage, code, message, data) {
84
+ this.emit('sync.warning', { stage, code, message, warningData: data });
85
+ }
86
+ emit(event, data = {}) {
87
+ const entry = {
88
+ ts: new Date().toISOString(),
89
+ event,
90
+ ciId: this.ctx.ciId,
91
+ integration: this.ctx.integration ?? null,
92
+ runId: this.ctx.runId ?? null,
93
+ ...data,
94
+ };
95
+ const line = JSON.stringify(entry);
96
+ if (event === 'sync.run.fail' || event === 'sync.record.error') {
97
+ console.error(line);
98
+ }
99
+ else if (event === 'sync.warning') {
100
+ console.warn(line);
101
+ }
102
+ else {
103
+ console.log(line);
104
+ }
105
+ this.forwardToEmitter(event, data);
106
+ }
107
+ /**
108
+ * Best-effort mirror of a sync event into the durable artifact stream. Any
109
+ * failure here is swallowed — structured logging must never break a sync.
110
+ */
111
+ forwardToEmitter(event, data) {
112
+ const emitter = this.emitter;
113
+ if (!emitter)
114
+ return;
115
+ try {
116
+ const stage = this.stageLabel(data);
117
+ switch (event) {
118
+ case 'sync.entity-map.start':
119
+ emitter.stageStart(stage, `Syncing ${stage}`);
120
+ break;
121
+ case 'sync.fetch.batch.complete':
122
+ emitter.emit('records.batch.complete', {
123
+ stage,
124
+ counts: { processed: this.num(data.recordCount) },
125
+ data,
126
+ });
127
+ break;
128
+ case 'sync.record.error':
129
+ emitter.emit('record.error', {
130
+ stage,
131
+ level: 'error',
132
+ message: typeof data.error === 'string' ? data.error : 'record error',
133
+ data,
134
+ });
135
+ break;
136
+ case 'sync.record.conflict':
137
+ emitter.emit('record.error', { stage, level: 'warn', message: 'conflict', data });
138
+ break;
139
+ case 'sync.warning':
140
+ emitter.warning(typeof data.stage === 'string' ? data.stage : stage, typeof data.code === 'string' ? data.code : 'WARNING', typeof data.message === 'string' ? data.message : '', this.asRecord(data.warningData));
141
+ break;
142
+ case 'sync.entity-map.complete':
143
+ emitter.stageComplete(stage, {
144
+ processed: this.num(data.recordsProcessed),
145
+ succeeded: this.num(data.recordsCreated) + this.num(data.recordsUpdated),
146
+ failed: this.num(data.recordsErrored),
147
+ skipped: this.num(data.recordsSkipped),
148
+ });
149
+ break;
150
+ case 'sync.push.response':
151
+ emitter.emit('external.call.complete', { stage, level: 'debug', data });
152
+ break;
153
+ default:
154
+ // run.start / config.loaded / connector.* / fetch.start / record.decision|saved /
155
+ // push.candidates|record / terminal events are intentionally not mirrored — the
156
+ // caller owns run lifecycle, and per-record events would bloat the stream.
157
+ break;
158
+ }
159
+ }
160
+ catch {
161
+ /* structured-artifact mirroring is best-effort — never break a sync */
162
+ }
163
+ }
164
+ stageLabel(data) {
165
+ const s = data.externalObjectName ?? data.entityMap ?? data.entity ?? data.objectName;
166
+ return typeof s === 'string' && s.length > 0 ? s : 'sync';
167
+ }
168
+ num(v) {
169
+ return typeof v === 'number' && Number.isFinite(v) ? v : 0;
170
+ }
171
+ /** Narrows an unknown to a plain record (or undefined) without a lazy `any` cast. */
172
+ asRecord(v) {
173
+ return v !== null && typeof v === 'object' ? v : undefined;
174
+ }
175
+ }
176
+ //# sourceMappingURL=SyncLogger.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"SyncLogger.js","sourceRoot":"","sources":["../src/SyncLogger.ts"],"names":[],"mappings":"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"}
@@ -43,13 +43,48 @@ export declare class WatermarkService {
43
43
  * @returns true if the watermark is valid for its type, false otherwise
44
44
  */
45
45
  ValidateWatermark(watermarkValue: string, watermarkType: WatermarkType): boolean;
46
+ /**
47
+ * Persists a keyset/seek resume position for an INTERRUPTED scan (plan.md §8a).
48
+ *
49
+ * Stored on the Pull watermark record as `WatermarkType='Cursor'` so {@link Load} surfaces it and
50
+ * `ProcessPullSync` can resume the seek (`key > AfterKey`) on the next run instead of re-scanning
51
+ * from the start. Only meaningful for connectors that declare a `StableOrderingKey`; the engine
52
+ * gates the call on that, so a timestamp connector's record is never written here. Idempotent —
53
+ * safe to call repeatedly at the checkpoint cadence and once more on graceful early-exit.
54
+ */
55
+ SaveKeysetPosition(entityMapID: string, afterKey: string, contextUser: UserInfo): Promise<void>;
56
+ /**
57
+ * Clears a persisted keyset resume position after a CLEAN scan (plan.md §8a), so the next run
58
+ * starts a fresh seek from the beginning. Keeps the record (still `WatermarkType='Cursor'`) for
59
+ * bookkeeping but nulls the value — a null value is what the restore logic reads as "no scan in
60
+ * progress". No-op when no watermark record exists yet.
61
+ */
62
+ ClearKeysetPosition(entityMapID: string, contextUser: UserInfo): Promise<void>;
63
+ /**
64
+ * Persists the partition→rollup map for a partition hash-diff (Merkle) reconcile of a watermark-less
65
+ * source (plan.md §7). Stored on the Pull watermark record as `WatermarkType='ChangeToken'` (the
66
+ * map IS an opaque change-detection token), JSON-encoded. The object is no-watermark, so this field
67
+ * is free; a partition-reconcile object never also carries a timestamp/cursor. Idempotent upsert.
68
+ */
69
+ SavePartitionRollups(entityMapID: string, rollups: Map<string, string>, contextUser: UserInfo): Promise<void>;
70
+ /**
71
+ * Loads the previously-stored partition→rollup map (empty map when none / first sync / wrong type),
72
+ * so the next reconcile can diff against it and deep-sync only the partitions whose rollup moved.
73
+ */
74
+ LoadPartitionRollups(entityMapID: string, contextUser: UserInfo): Promise<Map<string, string>>;
46
75
  /**
47
76
  * Updates an existing watermark record with a new value and timestamp.
48
77
  */
49
78
  private UpdateExistingWatermark;
50
79
  /**
51
- * Creates a new watermark record for the given entity map.
80
+ * Creates a new watermark record for the given entity map (Timestamp type — the default for
81
+ * incremental connectors).
52
82
  */
53
83
  private CreateNewWatermark;
84
+ /**
85
+ * Shared creator for a new watermark row. Factored out so timestamp watermarks and keyset resume
86
+ * positions ({@link SaveKeysetPosition}) share one persistence path with a per-call WatermarkType.
87
+ */
88
+ private createWatermark;
54
89
  }
55
90
  //# sourceMappingURL=WatermarkService.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"WatermarkService.d.ts","sourceRoot":"","sources":["../src/WatermarkService.ts"],"names":[],"mappings":"AAAA,OAAO,EAAqB,KAAK,QAAQ,EAAE,MAAM,sBAAsB,CAAC;AAExE,OAAO,KAAK,EAAE,gCAAgC,EAAE,MAAM,mBAAmB,CAAC;AAC1E,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,YAAY,CAAC;AAEhD;;;GAGG;AACH,qBAAa,gBAAgB;IACzB;;;;;;OAMG;IACU,IAAI,CACb,WAAW,EAAE,MAAM,EACnB,WAAW,EAAE,QAAQ,EACrB,SAAS,GAAE,MAAM,GAAG,MAAe,GACpC,OAAO,CAAC,gCAAgC,GAAG,IAAI,CAAC;IAcnD;;;;;;;;OAQG;IACU,MAAM,CACf,WAAW,EAAE,MAAM,EACnB,QAAQ,EAAE,MAAM,EAChB,WAAW,EAAE,QAAQ,EACrB,SAAS,GAAE,MAAM,GAAG,MAAe,GACpC,OAAO,CAAC,IAAI,CAAC;IAShB;;;;;;;;;OASG;IACU,cAAc,CACvB,WAAW,EAAE,MAAM,EACnB,YAAY,EAAE,MAAM,EACpB,WAAW,EAAE,QAAQ,GACtB,OAAO,CAAC,IAAI,CAAC;IAShB;;;;;;OAMG;IACI,iBAAiB,CAAC,cAAc,EAAE,MAAM,EAAE,aAAa,EAAE,aAAa,GAAG,OAAO;IAkBvF;;OAEG;YACW,uBAAuB;IAYrC;;OAEG;YACW,kBAAkB;CAyBnC"}
1
+ {"version":3,"file":"WatermarkService.d.ts","sourceRoot":"","sources":["../src/WatermarkService.ts"],"names":[],"mappings":"AAAA,OAAO,EAAqB,KAAK,QAAQ,EAAE,MAAM,sBAAsB,CAAC;AAExE,OAAO,KAAK,EAAE,gCAAgC,EAAE,MAAM,mBAAmB,CAAC;AAC1E,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,YAAY,CAAC;AAEhD;;;GAGG;AACH,qBAAa,gBAAgB;IACzB;;;;;;OAMG;IACU,IAAI,CACb,WAAW,EAAE,MAAM,EACnB,WAAW,EAAE,QAAQ,EACrB,SAAS,GAAE,MAAM,GAAG,MAAe,GACpC,OAAO,CAAC,gCAAgC,GAAG,IAAI,CAAC;IAsBnD;;;;;;;;OAQG;IACU,MAAM,CACf,WAAW,EAAE,MAAM,EACnB,QAAQ,EAAE,MAAM,EAChB,WAAW,EAAE,QAAQ,EACrB,SAAS,GAAE,MAAM,GAAG,MAAe,GACpC,OAAO,CAAC,IAAI,CAAC;IAShB;;;;;;;;;OASG;IACU,cAAc,CACvB,WAAW,EAAE,MAAM,EACnB,YAAY,EAAE,MAAM,EACpB,WAAW,EAAE,QAAQ,GACtB,OAAO,CAAC,IAAI,CAAC;IAShB;;;;;;OAMG;IACI,iBAAiB,CAAC,cAAc,EAAE,MAAM,EAAE,aAAa,EAAE,aAAa,GAAG,OAAO;IAkBvF;;;;;;;;OAQG;IACU,kBAAkB,CAC3B,WAAW,EAAE,MAAM,EACnB,QAAQ,EAAE,MAAM,EAChB,WAAW,EAAE,QAAQ,GACtB,OAAO,CAAC,IAAI,CAAC;IAehB;;;;;OAKG;IACU,mBAAmB,CAAC,WAAW,EAAE,MAAM,EAAE,WAAW,EAAE,QAAQ,GAAG,OAAO,CAAC,IAAI,CAAC;IAY3F;;;;;OAKG;IACU,oBAAoB,CAC7B,WAAW,EAAE,MAAM,EACnB,OAAO,EAAE,GAAG,CAAC,MAAM,EAAE,MAAM,CAAC,EAC5B,WAAW,EAAE,QAAQ,GACtB,OAAO,CAAC,IAAI,CAAC;IAchB;;;OAGG;IACU,oBAAoB,CAC7B,WAAW,EAAE,MAAM,EACnB,WAAW,EAAE,QAAQ,GACtB,OAAO,CAAC,GAAG,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;IAa/B;;OAEG;YACW,uBAAuB;IAYrC;;;OAGG;YACW,kBAAkB;IAShC;;;OAGG;YACW,eAAe;CAsChC"}