@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.
- package/dist/ActionMetadataGenerator.d.ts +8 -1
- package/dist/ActionMetadataGenerator.d.ts.map +1 -1
- package/dist/ActionMetadataGenerator.js +22 -3
- package/dist/ActionMetadataGenerator.js.map +1 -1
- package/dist/AdaptiveConcurrency.d.ts +85 -0
- package/dist/AdaptiveConcurrency.d.ts.map +1 -0
- package/dist/AdaptiveConcurrency.js +148 -0
- package/dist/AdaptiveConcurrency.js.map +1 -0
- package/dist/BaseIntegrationConnector.d.ts +127 -3
- package/dist/BaseIntegrationConnector.d.ts.map +1 -1
- package/dist/BaseIntegrationConnector.js +126 -11
- package/dist/BaseIntegrationConnector.js.map +1 -1
- package/dist/BaseRESTIntegrationConnector.d.ts +80 -15
- package/dist/BaseRESTIntegrationConnector.d.ts.map +1 -1
- package/dist/BaseRESTIntegrationConnector.js +314 -64
- package/dist/BaseRESTIntegrationConnector.js.map +1 -1
- package/dist/ConflictRecency.d.ts +24 -0
- package/dist/ConflictRecency.d.ts.map +1 -0
- package/dist/ConflictRecency.js +25 -0
- package/dist/ConflictRecency.js.map +1 -0
- package/dist/ContentHash.d.ts +28 -0
- package/dist/ContentHash.d.ts.map +1 -0
- package/dist/ContentHash.js +54 -0
- package/dist/ContentHash.js.map +1 -0
- package/dist/EnrichSchemaConstraints.d.ts +59 -0
- package/dist/EnrichSchemaConstraints.d.ts.map +1 -0
- package/dist/EnrichSchemaConstraints.js +168 -0
- package/dist/EnrichSchemaConstraints.js.map +1 -0
- package/dist/FieldMappingEngine.d.ts +22 -0
- package/dist/FieldMappingEngine.d.ts.map +1 -1
- package/dist/FieldMappingEngine.js +66 -5
- package/dist/FieldMappingEngine.js.map +1 -1
- package/dist/HashDiff.d.ts +68 -0
- package/dist/HashDiff.d.ts.map +1 -0
- package/dist/HashDiff.js +108 -0
- package/dist/HashDiff.js.map +1 -0
- package/dist/IntegrationActionGenerator.d.ts +93 -0
- package/dist/IntegrationActionGenerator.d.ts.map +1 -0
- package/dist/IntegrationActionGenerator.js +313 -0
- package/dist/IntegrationActionGenerator.js.map +1 -0
- package/dist/IntegrationConnectorCreationPipeline.d.ts +86 -0
- package/dist/IntegrationConnectorCreationPipeline.d.ts.map +1 -0
- package/dist/IntegrationConnectorCreationPipeline.js +226 -0
- package/dist/IntegrationConnectorCreationPipeline.js.map +1 -0
- package/dist/IntegrationEngine.d.ts +199 -1
- package/dist/IntegrationEngine.d.ts.map +1 -1
- package/dist/IntegrationEngine.js +1592 -109
- package/dist/IntegrationEngine.js.map +1 -1
- package/dist/IntegrationSchemaSync.d.ts +82 -0
- package/dist/IntegrationSchemaSync.d.ts.map +1 -1
- package/dist/IntegrationSchemaSync.js +289 -42
- package/dist/IntegrationSchemaSync.js.map +1 -1
- package/dist/MatchEngine.d.ts.map +1 -1
- package/dist/MatchEngine.js +4 -0
- package/dist/MatchEngine.js.map +1 -1
- package/dist/RateLimiter.d.ts +117 -0
- package/dist/RateLimiter.d.ts.map +1 -0
- package/dist/RateLimiter.js +159 -0
- package/dist/RateLimiter.js.map +1 -0
- package/dist/SyncLogger.d.ts +106 -0
- package/dist/SyncLogger.d.ts.map +1 -0
- package/dist/SyncLogger.js +176 -0
- package/dist/SyncLogger.js.map +1 -0
- package/dist/WatermarkService.d.ts +36 -1
- package/dist/WatermarkService.d.ts.map +1 -1
- package/dist/WatermarkService.js +113 -3
- package/dist/WatermarkService.js.map +1 -1
- package/dist/index.d.ts +23 -6
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +11 -1
- package/dist/index.js.map +1 -1
- package/dist/types.d.ts +52 -1
- package/dist/types.d.ts.map +1 -1
- 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;
|
|
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"}
|