@memberjunction/integration-engine 6.1.0-edge.2 → 6.1.0-edge.4

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 (77) hide show
  1. package/LICENSE +180 -4
  2. package/dist/BaseIntegrationConnector.d.ts +115 -4
  3. package/dist/BaseIntegrationConnector.d.ts.map +1 -1
  4. package/dist/BaseIntegrationConnector.js +191 -24
  5. package/dist/BaseIntegrationConnector.js.map +1 -1
  6. package/dist/BaseRESTIntegrationConnector.d.ts +14 -1
  7. package/dist/BaseRESTIntegrationConnector.d.ts.map +1 -1
  8. package/dist/BaseRESTIntegrationConnector.js +50 -3
  9. package/dist/BaseRESTIntegrationConnector.js.map +1 -1
  10. package/dist/BatchIdentity.d.ts +47 -0
  11. package/dist/BatchIdentity.d.ts.map +1 -0
  12. package/dist/BatchIdentity.js +78 -0
  13. package/dist/BatchIdentity.js.map +1 -0
  14. package/dist/DiscoveryWatchdog.d.ts +103 -0
  15. package/dist/DiscoveryWatchdog.d.ts.map +1 -0
  16. package/dist/DiscoveryWatchdog.js +166 -0
  17. package/dist/DiscoveryWatchdog.js.map +1 -0
  18. package/dist/FieldMapValidation.d.ts +40 -0
  19. package/dist/FieldMapValidation.d.ts.map +1 -0
  20. package/dist/FieldMapValidation.js +58 -0
  21. package/dist/FieldMapValidation.js.map +1 -0
  22. package/dist/FieldMappingEngine.d.ts +1 -13
  23. package/dist/FieldMappingEngine.d.ts.map +1 -1
  24. package/dist/FieldMappingEngine.js +18 -3
  25. package/dist/FieldMappingEngine.js.map +1 -1
  26. package/dist/IntegrationConnectorCreationPipeline.d.ts +92 -1
  27. package/dist/IntegrationConnectorCreationPipeline.d.ts.map +1 -1
  28. package/dist/IntegrationConnectorCreationPipeline.js +186 -11
  29. package/dist/IntegrationConnectorCreationPipeline.js.map +1 -1
  30. package/dist/IntegrationEngine.d.ts +198 -21
  31. package/dist/IntegrationEngine.d.ts.map +1 -1
  32. package/dist/IntegrationEngine.js +1092 -165
  33. package/dist/IntegrationEngine.js.map +1 -1
  34. package/dist/IntegrationSchemaSync.d.ts +8 -0
  35. package/dist/IntegrationSchemaSync.d.ts.map +1 -1
  36. package/dist/IntegrationSchemaSync.js +39 -2
  37. package/dist/IntegrationSchemaSync.js.map +1 -1
  38. package/dist/KeylessRecordGuard.d.ts +56 -0
  39. package/dist/KeylessRecordGuard.d.ts.map +1 -0
  40. package/dist/KeylessRecordGuard.js +80 -0
  41. package/dist/KeylessRecordGuard.js.map +1 -0
  42. package/dist/ResumeConcurrency.d.ts +46 -0
  43. package/dist/ResumeConcurrency.d.ts.map +1 -0
  44. package/dist/ResumeConcurrency.js +74 -0
  45. package/dist/ResumeConcurrency.js.map +1 -0
  46. package/dist/RetryAfter.d.ts +50 -0
  47. package/dist/RetryAfter.d.ts.map +1 -0
  48. package/dist/RetryAfter.js +127 -0
  49. package/dist/RetryAfter.js.map +1 -0
  50. package/dist/RetryRunner.d.ts +26 -2
  51. package/dist/RetryRunner.d.ts.map +1 -1
  52. package/dist/RetryRunner.js +11 -3
  53. package/dist/RetryRunner.js.map +1 -1
  54. package/dist/RunOwnershipService.d.ts +177 -0
  55. package/dist/RunOwnershipService.d.ts.map +1 -0
  56. package/dist/RunOwnershipService.js +279 -0
  57. package/dist/RunOwnershipService.js.map +1 -0
  58. package/dist/SyncDirectives.d.ts +66 -0
  59. package/dist/SyncDirectives.d.ts.map +1 -0
  60. package/dist/SyncDirectives.js +123 -0
  61. package/dist/SyncDirectives.js.map +1 -0
  62. package/dist/SyncLogger.d.ts +1 -1
  63. package/dist/SyncLogger.d.ts.map +1 -1
  64. package/dist/SyncLogger.js.map +1 -1
  65. package/dist/WatermarkService.d.ts +8 -0
  66. package/dist/WatermarkService.d.ts.map +1 -1
  67. package/dist/WatermarkService.js +18 -0
  68. package/dist/WatermarkService.js.map +1 -1
  69. package/dist/index.d.ts +12 -1
  70. package/dist/index.d.ts.map +1 -1
  71. package/dist/index.js +9 -1
  72. package/dist/index.js.map +1 -1
  73. package/dist/types.d.ts +28 -0
  74. package/dist/types.d.ts.map +1 -1
  75. package/dist/types.js +66 -1
  76. package/dist/types.js.map +1 -1
  77. package/package.json +8 -8
@@ -0,0 +1,127 @@
1
+ /**
2
+ * @fileoverview Standard `Retry-After` extraction, so a connector that writes no rate-limit code
3
+ * still backs off by the amount the vendor actually asked for.
4
+ *
5
+ * `BaseIntegrationConnector.ExtractRetryAfterMs` returned `undefined` unconditionally and no
6
+ * connector in this repo overrode it. The engine's limiter therefore always fell back to its own
7
+ * multiplicative decrease — correct in direction, but blind to a number the vendor had already
8
+ * supplied in the response.
9
+ *
10
+ * `Retry-After` is defined by RFC 9110 §10.2.3 and is the header a 429 (RFC 6585) and a 503 carry.
11
+ * Parsing it is not a heuristic and not vendor-specific, which is exactly why it belongs in the
12
+ * base class rather than in each connector: there is one correct reading of it, and every HTTP
13
+ * connector benefits from having it read.
14
+ *
15
+ * Deliberately NOT a message-text parser. Guessing a duration out of prose ("try again in a bit")
16
+ * risks inventing a number, and a wrong Retry-After is worse than none — it would freeze the bucket
17
+ * for a made-up interval. Text-shaped signals stay the connector's job (see PheedLoop, whose vendor
18
+ * puts its delay in the body rather than a header); this reads the standard header only.
19
+ */
20
+ /**
21
+ * Upper bound on an honored `Retry-After`, in milliseconds (5 minutes).
22
+ *
23
+ * A vendor occasionally returns an enormous or malformed value — a Unix timestamp mistaken for
24
+ * delay-seconds, a date years out. Freezing a sync's token bucket for hours on the strength of one
25
+ * response is a worse failure than backing off by the limiter's own decrease, so anything beyond
26
+ * this is treated as unusable and the limiter's default applies.
27
+ */
28
+ export const MAX_HONORED_RETRY_AFTER_MS = 5 * 60_000;
29
+ /**
30
+ * Reads one header by name from a `Headers` instance, a `Map`, or a plain object, case-insensitively.
31
+ * Returns undefined when the container is not one of those or the header is absent.
32
+ */
33
+ function readHeader(container, name) {
34
+ if (!container || typeof container !== 'object')
35
+ return undefined;
36
+ // fetch's Headers (and anything else exposing a case-insensitive get)
37
+ const getter = container.get;
38
+ if (typeof getter === 'function') {
39
+ try {
40
+ const v = container.get(name);
41
+ if (typeof v === 'string' && v.length > 0)
42
+ return v;
43
+ }
44
+ catch { /* not a Headers-like after all */ }
45
+ }
46
+ // Plain object / axios-style header bag. Node lower-cases response header names, but a hand-built
47
+ // object may not, so compare case-insensitively rather than trusting the spelling.
48
+ const target = name.toLowerCase();
49
+ for (const [k, v] of Object.entries(container)) {
50
+ if (k.toLowerCase() !== target)
51
+ continue;
52
+ // Node can hand back string[] for a repeated header; the first value is the one that counts.
53
+ const raw = Array.isArray(v) ? v[0] : v;
54
+ if (typeof raw === 'string' && raw.length > 0)
55
+ return raw;
56
+ if (typeof raw === 'number' && Number.isFinite(raw))
57
+ return String(raw);
58
+ }
59
+ return undefined;
60
+ }
61
+ /**
62
+ * Parses a `Retry-After` value into milliseconds. RFC 9110 permits two forms:
63
+ * - delay-seconds: a non-negative integer, e.g. `120`
64
+ * - HTTP-date: e.g. `Wed, 21 Oct 2015 07:28:00 GMT`
65
+ *
66
+ * @param value the raw header value
67
+ * @param nowMs clock reading used to convert an HTTP-date into a delay; injectable for tests
68
+ * @returns milliseconds to wait, or undefined when the value is unusable
69
+ */
70
+ export function ParseRetryAfterValue(value, nowMs = Date.now()) {
71
+ if (!value)
72
+ return undefined;
73
+ const trimmed = value.trim();
74
+ if (trimmed.length === 0)
75
+ return undefined;
76
+ // delay-seconds. Accept only a bare integer: a decimal or anything with trailing junk is not
77
+ // what the spec defines, and Number() would happily coerce forms that mean something else.
78
+ if (/^\d+$/.test(trimmed)) {
79
+ const ms = Number(trimmed) * 1000;
80
+ return ms > 0 && ms <= MAX_HONORED_RETRY_AFTER_MS ? ms : undefined;
81
+ }
82
+ // HTTP-date. `Date.parse` is extremely permissive — it happily reads '1.5' as a date and
83
+ // returns a real timestamp — so gate it on the string actually LOOKING like one first.
84
+ // Every form RFC 9110 permits (IMF-fixdate, obsolete RFC 850, asctime) carries a three-letter
85
+ // day or month name AND a digit; the junk that reaches here ('1.5', '30s', '1e3', 'soon')
86
+ // carries at most one of the two. Falling through to a misparsed date would invent a delay,
87
+ // which is the one outcome worse than having none.
88
+ if (!/[A-Za-z]{3}/.test(trimmed) || !/\d/.test(trimmed))
89
+ return undefined;
90
+ // A date already in the past means "retry now" — no wait, but also not a parse failure, so it
91
+ // must not fall through to the limiter's default as though nothing was said.
92
+ const at = Date.parse(trimmed);
93
+ if (Number.isNaN(at))
94
+ return undefined;
95
+ const ms = at - nowMs;
96
+ if (ms <= 0)
97
+ return 0;
98
+ return ms <= MAX_HONORED_RETRY_AFTER_MS ? ms : undefined;
99
+ }
100
+ /**
101
+ * Extracts a `Retry-After` delay, in milliseconds, from a thrown error or a failed response.
102
+ *
103
+ * Walks the shapes HTTP clients actually throw: the error itself carrying `headers`, an axios-style
104
+ * `error.response.headers`, and one level of `error.cause` (how a wrapper preserves the original).
105
+ *
106
+ * @param error the thrown value
107
+ * @param nowMs clock reading for HTTP-date conversion; injectable for tests
108
+ * @returns milliseconds to wait, or undefined when no usable header is present
109
+ */
110
+ export function ExtractRetryAfterFromError(error, nowMs = Date.now()) {
111
+ if (!error || typeof error !== 'object')
112
+ return undefined;
113
+ const e = error;
114
+ const candidates = [
115
+ e.headers,
116
+ e.response?.headers,
117
+ e.cause?.headers,
118
+ e.cause?.response?.headers,
119
+ ];
120
+ for (const container of candidates) {
121
+ const parsed = ParseRetryAfterValue(readHeader(container, 'retry-after'), nowMs);
122
+ if (parsed !== undefined)
123
+ return parsed;
124
+ }
125
+ return undefined;
126
+ }
127
+ //# sourceMappingURL=RetryAfter.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"RetryAfter.js","sourceRoot":"","sources":["../src/RetryAfter.ts"],"names":[],"mappings":"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"}
@@ -13,15 +13,39 @@ export interface RetryConfig {
13
13
  }
14
14
  /** Default retry configuration */
15
15
  export declare const DEFAULT_RETRY_CONFIG: RetryConfig;
16
+ /**
17
+ * Optional hooks that let a caller make the wait between attempts smarter than blind backoff.
18
+ *
19
+ * Both exist for rate limiting. Exponential backoff is the right default for a network blip, but it
20
+ * is the WRONG answer to a 429: the source has usually told us exactly how long to wait, and
21
+ * retrying sooner is what turns a soft throttle into a hard one.
22
+ */
23
+ export interface RetryHooks {
24
+ /**
25
+ * Derive the wait from the error itself — e.g. a `Retry-After` header. Return `null`/`undefined`
26
+ * to keep the computed exponential backoff. The larger of the two is NOT taken automatically;
27
+ * returning a value replaces the backoff, so a caller can honour a source that asks for a
28
+ * SHORTER wait as well as a longer one.
29
+ */
30
+ DelayForError?: (error: unknown, attempt: number, backoffMs: number) => number | null | undefined;
31
+ /**
32
+ * Awaited after the delay and before the next attempt — e.g. re-acquiring a rate-limit token, so
33
+ * a retry passes through the same gate the first attempt did instead of bypassing it.
34
+ */
35
+ BeforeRetry?: (attempt: number, error: unknown) => Promise<void> | void;
36
+ }
16
37
  /**
17
38
  * Executes an operation with retry logic using exponential backoff.
18
39
  *
19
40
  * @param operation - The async operation to execute
20
41
  * @param config - Retry configuration (uses defaults if not provided)
21
42
  * @param isRetryable - Predicate to determine if a caught error should trigger a retry
22
- * @param onRetry - Optional callback invoked before each retry with attempt number, error, and delay
43
+ * @param onRetry - Optional callback invoked before each retry with attempt number, error, and delay.
44
+ * Runs BEFORE the wait, so it is the right place to report the failure onward (e.g.
45
+ * backing off a shared limiter) rather than after the retries are spent.
46
+ * @param hooks - Optional {@link RetryHooks} for source-directed delays and pre-attempt gating
23
47
  * @returns The result of the operation
24
48
  * @throws The last error encountered if all attempts fail
25
49
  */
26
- export declare function WithRetry<T>(operation: () => Promise<T>, config?: RetryConfig, isRetryable?: (error: unknown) => boolean, onRetry?: (attempt: number, error: unknown, delayMs: number) => void): Promise<T>;
50
+ export declare function WithRetry<T>(operation: () => Promise<T>, config?: RetryConfig, isRetryable?: (error: unknown) => boolean, onRetry?: (attempt: number, error: unknown, delayMs: number) => void, hooks?: RetryHooks): Promise<T>;
27
51
  //# sourceMappingURL=RetryRunner.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"RetryRunner.d.ts","sourceRoot":"","sources":["../src/RetryRunner.ts"],"names":[],"mappings":"AAAA;;GAEG;AACH,MAAM,WAAW,WAAW;IACxB,yEAAyE;IACzE,WAAW,EAAE,MAAM,CAAC;IACpB,2DAA2D;IAC3D,gBAAgB,EAAE,MAAM,CAAC;IACzB,4DAA4D;IAC5D,YAAY,EAAE,MAAM,CAAC;IACrB,oDAAoD;IACpD,cAAc,EAAE,MAAM,CAAC;CAC1B;AAED,kCAAkC;AAClC,eAAO,MAAM,oBAAoB,EAAE,WAKlC,CAAC;AAeF;;;;;;;;;GASG;AACH,wBAAsB,SAAS,CAAC,CAAC,EAC7B,SAAS,EAAE,MAAM,OAAO,CAAC,CAAC,CAAC,EAC3B,MAAM,GAAE,WAAkC,EAC1C,WAAW,GAAE,CAAC,KAAK,EAAE,OAAO,KAAK,OAAoB,EACrD,OAAO,CAAC,EAAE,CAAC,OAAO,EAAE,MAAM,EAAE,KAAK,EAAE,OAAO,EAAE,OAAO,EAAE,MAAM,KAAK,IAAI,GACrE,OAAO,CAAC,CAAC,CAAC,CAyBZ"}
1
+ {"version":3,"file":"RetryRunner.d.ts","sourceRoot":"","sources":["../src/RetryRunner.ts"],"names":[],"mappings":"AAAA;;GAEG;AACH,MAAM,WAAW,WAAW;IACxB,yEAAyE;IACzE,WAAW,EAAE,MAAM,CAAC;IACpB,2DAA2D;IAC3D,gBAAgB,EAAE,MAAM,CAAC;IACzB,4DAA4D;IAC5D,YAAY,EAAE,MAAM,CAAC;IACrB,oDAAoD;IACpD,cAAc,EAAE,MAAM,CAAC;CAC1B;AAED,kCAAkC;AAClC,eAAO,MAAM,oBAAoB,EAAE,WAKlC,CAAC;AAeF;;;;;;GAMG;AACH,MAAM,WAAW,UAAU;IACvB;;;;;OAKG;IACH,aAAa,CAAC,EAAE,CAAC,KAAK,EAAE,OAAO,EAAE,OAAO,EAAE,MAAM,EAAE,SAAS,EAAE,MAAM,KAAK,MAAM,GAAG,IAAI,GAAG,SAAS,CAAC;IAClG;;;OAGG;IACH,WAAW,CAAC,EAAE,CAAC,OAAO,EAAE,MAAM,EAAE,KAAK,EAAE,OAAO,KAAK,OAAO,CAAC,IAAI,CAAC,GAAG,IAAI,CAAC;CAC3E;AAED;;;;;;;;;;;;GAYG;AACH,wBAAsB,SAAS,CAAC,CAAC,EAC7B,SAAS,EAAE,MAAM,OAAO,CAAC,CAAC,CAAC,EAC3B,MAAM,GAAE,WAAkC,EAC1C,WAAW,GAAE,CAAC,KAAK,EAAE,OAAO,KAAK,OAAoB,EACrD,OAAO,CAAC,EAAE,CAAC,OAAO,EAAE,MAAM,EAAE,KAAK,EAAE,OAAO,EAAE,OAAO,EAAE,MAAM,KAAK,IAAI,EACpE,KAAK,CAAC,EAAE,UAAU,GACnB,OAAO,CAAC,CAAC,CAAC,CA+BZ"}
@@ -23,11 +23,14 @@ function computeDelay(attempt, config) {
23
23
  * @param operation - The async operation to execute
24
24
  * @param config - Retry configuration (uses defaults if not provided)
25
25
  * @param isRetryable - Predicate to determine if a caught error should trigger a retry
26
- * @param onRetry - Optional callback invoked before each retry with attempt number, error, and delay
26
+ * @param onRetry - Optional callback invoked before each retry with attempt number, error, and delay.
27
+ * Runs BEFORE the wait, so it is the right place to report the failure onward (e.g.
28
+ * backing off a shared limiter) rather than after the retries are spent.
29
+ * @param hooks - Optional {@link RetryHooks} for source-directed delays and pre-attempt gating
27
30
  * @returns The result of the operation
28
31
  * @throws The last error encountered if all attempts fail
29
32
  */
30
- export async function WithRetry(operation, config = DEFAULT_RETRY_CONFIG, isRetryable = () => true, onRetry) {
33
+ export async function WithRetry(operation, config = DEFAULT_RETRY_CONFIG, isRetryable = () => true, onRetry, hooks) {
31
34
  let lastError;
32
35
  for (let attempt = 1; attempt <= config.MaxAttempts; attempt++) {
33
36
  try {
@@ -39,11 +42,16 @@ export async function WithRetry(operation, config = DEFAULT_RETRY_CONFIG, isRetr
39
42
  if (isLastAttempt || !isRetryable(err)) {
40
43
  throw err;
41
44
  }
42
- const delayMs = computeDelay(attempt, config);
45
+ const backoffMs = computeDelay(attempt, config);
46
+ const directed = hooks?.DelayForError?.(err, attempt, backoffMs);
47
+ const delayMs = typeof directed === 'number' && Number.isFinite(directed) && directed >= 0
48
+ ? directed
49
+ : backoffMs;
43
50
  if (onRetry) {
44
51
  onRetry(attempt, err, delayMs);
45
52
  }
46
53
  await sleep(delayMs);
54
+ await hooks?.BeforeRetry?.(attempt, err);
47
55
  }
48
56
  }
49
57
  // Should not reach here, but TypeScript needs this
@@ -1 +1 @@
1
- {"version":3,"file":"RetryRunner.js","sourceRoot":"","sources":["../src/RetryRunner.ts"],"names":[],"mappings":"AAcA,kCAAkC;AAClC,MAAM,CAAC,MAAM,oBAAoB,GAAgB;IAC7C,WAAW,EAAE,CAAC;IACd,gBAAgB,EAAE,IAAI;IACtB,YAAY,EAAE,KAAK;IACnB,cAAc,EAAE,GAAG;CACtB,CAAC;AAEF;;;;;GAKG;AACH,SAAS,YAAY,CAAC,OAAe,EAAE,MAAmB;IACtD,MAAM,gBAAgB,GAAG,MAAM,CAAC,gBAAgB,GAAG,IAAI,CAAC,GAAG,CAAC,CAAC,EAAE,OAAO,GAAG,CAAC,CAAC,CAAC;IAC5E,MAAM,MAAM,GAAG,IAAI,CAAC,GAAG,CAAC,gBAAgB,EAAE,MAAM,CAAC,YAAY,CAAC,CAAC;IAC/D,MAAM,MAAM,GAAG,MAAM,GAAG,MAAM,CAAC,cAAc,GAAG,IAAI,CAAC,MAAM,EAAE,CAAC;IAC9D,OAAO,MAAM,GAAG,MAAM,CAAC;AAC3B,CAAC;AAED;;;;;;;;;GASG;AACH,MAAM,CAAC,KAAK,UAAU,SAAS,CAC3B,SAA2B,EAC3B,SAAsB,oBAAoB,EAC1C,cAA2C,GAAG,EAAE,CAAC,IAAI,EACrD,OAAoE;IAEpE,IAAI,SAAkB,CAAC;IAEvB,KAAK,IAAI,OAAO,GAAG,CAAC,EAAE,OAAO,IAAI,MAAM,CAAC,WAAW,EAAE,OAAO,EAAE,EAAE,CAAC;QAC7D,IAAI,CAAC;YACD,OAAO,MAAM,SAAS,EAAE,CAAC;QAC7B,CAAC;QAAC,OAAO,GAAG,EAAE,CAAC;YACX,SAAS,GAAG,GAAG,CAAC;YAEhB,MAAM,aAAa,GAAG,OAAO,KAAK,MAAM,CAAC,WAAW,CAAC;YACrD,IAAI,aAAa,IAAI,CAAC,WAAW,CAAC,GAAG,CAAC,EAAE,CAAC;gBACrC,MAAM,GAAG,CAAC;YACd,CAAC;YAED,MAAM,OAAO,GAAG,YAAY,CAAC,OAAO,EAAE,MAAM,CAAC,CAAC;YAC9C,IAAI,OAAO,EAAE,CAAC;gBACV,OAAO,CAAC,OAAO,EAAE,GAAG,EAAE,OAAO,CAAC,CAAC;YACnC,CAAC;YAED,MAAM,KAAK,CAAC,OAAO,CAAC,CAAC;QACzB,CAAC;IACL,CAAC;IAED,mDAAmD;IACnD,MAAM,SAAS,CAAC;AACpB,CAAC;AAED;;;GAGG;AACH,SAAS,KAAK,CAAC,EAAU;IACrB,OAAO,IAAI,OAAO,CAAC,OAAO,CAAC,EAAE,CAAC,UAAU,CAAC,OAAO,EAAE,EAAE,CAAC,CAAC,CAAC;AAC3D,CAAC"}
1
+ {"version":3,"file":"RetryRunner.js","sourceRoot":"","sources":["../src/RetryRunner.ts"],"names":[],"mappings":"AAcA,kCAAkC;AAClC,MAAM,CAAC,MAAM,oBAAoB,GAAgB;IAC7C,WAAW,EAAE,CAAC;IACd,gBAAgB,EAAE,IAAI;IACtB,YAAY,EAAE,KAAK;IACnB,cAAc,EAAE,GAAG;CACtB,CAAC;AAEF;;;;;GAKG;AACH,SAAS,YAAY,CAAC,OAAe,EAAE,MAAmB;IACtD,MAAM,gBAAgB,GAAG,MAAM,CAAC,gBAAgB,GAAG,IAAI,CAAC,GAAG,CAAC,CAAC,EAAE,OAAO,GAAG,CAAC,CAAC,CAAC;IAC5E,MAAM,MAAM,GAAG,IAAI,CAAC,GAAG,CAAC,gBAAgB,EAAE,MAAM,CAAC,YAAY,CAAC,CAAC;IAC/D,MAAM,MAAM,GAAG,MAAM,GAAG,MAAM,CAAC,cAAc,GAAG,IAAI,CAAC,MAAM,EAAE,CAAC;IAC9D,OAAO,MAAM,GAAG,MAAM,CAAC;AAC3B,CAAC;AAwBD;;;;;;;;;;;;GAYG;AACH,MAAM,CAAC,KAAK,UAAU,SAAS,CAC3B,SAA2B,EAC3B,SAAsB,oBAAoB,EAC1C,cAA2C,GAAG,EAAE,CAAC,IAAI,EACrD,OAAoE,EACpE,KAAkB;IAElB,IAAI,SAAkB,CAAC;IAEvB,KAAK,IAAI,OAAO,GAAG,CAAC,EAAE,OAAO,IAAI,MAAM,CAAC,WAAW,EAAE,OAAO,EAAE,EAAE,CAAC;QAC7D,IAAI,CAAC;YACD,OAAO,MAAM,SAAS,EAAE,CAAC;QAC7B,CAAC;QAAC,OAAO,GAAG,EAAE,CAAC;YACX,SAAS,GAAG,GAAG,CAAC;YAEhB,MAAM,aAAa,GAAG,OAAO,KAAK,MAAM,CAAC,WAAW,CAAC;YACrD,IAAI,aAAa,IAAI,CAAC,WAAW,CAAC,GAAG,CAAC,EAAE,CAAC;gBACrC,MAAM,GAAG,CAAC;YACd,CAAC;YAED,MAAM,SAAS,GAAG,YAAY,CAAC,OAAO,EAAE,MAAM,CAAC,CAAC;YAChD,MAAM,QAAQ,GAAG,KAAK,EAAE,aAAa,EAAE,CAAC,GAAG,EAAE,OAAO,EAAE,SAAS,CAAC,CAAC;YACjE,MAAM,OAAO,GAAG,OAAO,QAAQ,KAAK,QAAQ,IAAI,MAAM,CAAC,QAAQ,CAAC,QAAQ,CAAC,IAAI,QAAQ,IAAI,CAAC;gBACtF,CAAC,CAAC,QAAQ;gBACV,CAAC,CAAC,SAAS,CAAC;YAEhB,IAAI,OAAO,EAAE,CAAC;gBACV,OAAO,CAAC,OAAO,EAAE,GAAG,EAAE,OAAO,CAAC,CAAC;YACnC,CAAC;YAED,MAAM,KAAK,CAAC,OAAO,CAAC,CAAC;YACrB,MAAM,KAAK,EAAE,WAAW,EAAE,CAAC,OAAO,EAAE,GAAG,CAAC,CAAC;QAC7C,CAAC;IACL,CAAC;IAED,mDAAmD;IACnD,MAAM,SAAS,CAAC;AACpB,CAAC;AAED;;;GAGG;AACH,SAAS,KAAK,CAAC,EAAU;IACrB,OAAO,IAAI,OAAO,CAAC,OAAO,CAAC,EAAE,CAAC,UAAU,CAAC,OAAO,EAAE,EAAE,CAAC,CAAC,CAAC;AAC3D,CAAC"}
@@ -0,0 +1,177 @@
1
+ import type { DatabaseProviderBase, UserInfo } from '@memberjunction/core';
2
+ import type { MJCompanyIntegrationRunEntity } from '@memberjunction/core-entities';
3
+ /**
4
+ * Thrown at a batch boundary when this process discovers it no longer owns the
5
+ * run row (lease reclaimed by a stale-sweep or another worker bumped the fence).
6
+ * The sync loop MUST let this propagate — per-record error handling must never
7
+ * swallow it, because continuing to write after ownership loss is exactly the
8
+ * split-brain hazard the fence exists to prevent.
9
+ */
10
+ /**
11
+ * The states a run can be released INTO — the terminal subset of the entity's Status value list.
12
+ * Derived with `Extract` rather than hand-written so it stays pinned to the CHECK-constraint union
13
+ * CodeGen generates: if one of these values were ever removed from the column, this type narrows and
14
+ * every call site fails to compile, instead of silently passing a status the constraint rejects at
15
+ * runtime. 'In Progress' / 'Pending' / 'Queued' are deliberately excluded — they are not terminal.
16
+ */
17
+ export type TerminalRunStatus = Extract<MJCompanyIntegrationRunEntity['Status'], 'Success' | 'Failed' | 'Cancelled'>;
18
+ export declare class RunOwnershipLostError extends Error {
19
+ readonly RunID: string;
20
+ constructor(runID: string, detail: string);
21
+ }
22
+ /** Result of a lease renewal attempt. */
23
+ export interface RenewResult {
24
+ /** True when the DB confirmed our token+fence still own the row and extended the lease. */
25
+ Renewed: boolean;
26
+ /** True when a cooperative cancel has been requested on the run row (only meaningful when Renewed). */
27
+ CancelRequested: boolean;
28
+ }
29
+ /** Result of a batch-boundary ownership check. */
30
+ export interface BoundaryCheckResult {
31
+ /** True when the row's OwnerToken + FenceToken still match ours. */
32
+ Owned: boolean;
33
+ /** True when CancelRequestedAt is set on the row. */
34
+ CancelRequested: boolean;
35
+ }
36
+ /** Options for the background heartbeat started via {@link RunOwnershipService.StartHeartbeat}. */
37
+ export interface HeartbeatOptions {
38
+ /**
39
+ * Called (once) when a renewal discovers ownership has been lost. The engine
40
+ * uses this to abort the in-flight sync locally; writes are additionally
41
+ * fenced at every batch boundary, so this is fast-fail, not the safety net.
42
+ */
43
+ onLost?: () => void;
44
+ /** Called (once per request) when a renewal observes CancelRequestedAt set. */
45
+ onCancelRequested?: () => void;
46
+ /** Supplies the latest progress JSON to piggyback on each renewal write. */
47
+ progressSupplier?: () => string | null;
48
+ }
49
+ /**
50
+ * Per-run ownership manager for durable CompanyIntegrationRun execution
51
+ * (GH tasks.md PR 1). One instance is created per sync run and owns:
52
+ *
53
+ * - **Claim**: a single atomic UPDATE...WHERE (unowned OR lease expired) via
54
+ * spClaimCompanyIntegrationRun — never select-then-update. Claiming bumps
55
+ * FenceToken, invalidating any prior holder's writes.
56
+ * - **Renew**: timer-driven lease extension (interval ≈ lease/3) via
57
+ * spRenewCompanyIntegrationRunLease, token+fence-checked. The renewal result
58
+ * doubles as the cross-process cancel signal (returns CancelRequestedAt).
59
+ * - **Boundary fence**: {@link CheckBoundary} — a cheap SELECT the sync loop
60
+ * calls before every batch's writes. Ownership lost ⇒ the loop throws
61
+ * {@link RunOwnershipLostError} and writes nothing further.
62
+ * - **Release**: terminal status write + owner clear, token-checked so a stale
63
+ * holder's release no-ops.
64
+ *
65
+ * All sproc calls use the dialect-portable positional-parameter convention
66
+ * (same pattern as ScheduledJobEngine's lock sprocs): SQL Server binds the
67
+ * sproc's named params to positional `@pN` placeholders via EXEC; PostgreSQL
68
+ * calls the plpgsql port via `SELECT * FROM fn($1,...)`. The SAME value array
69
+ * serves both.
70
+ */
71
+ export declare class RunOwnershipService {
72
+ /** Default lease when the caller supplies no MaxRuntimeMinutes. */
73
+ static readonly DEFAULT_LEASE_MINUTES = 10;
74
+ private readonly provider;
75
+ private readonly contextUser;
76
+ private readonly runID;
77
+ private readonly ownerToken;
78
+ private readonly leaseMinutes;
79
+ private fenceToken;
80
+ private lastKnownLeaseExpiresAt;
81
+ private heartbeatTimer;
82
+ private lostNotified;
83
+ private cancelNotified;
84
+ private renewInFlight;
85
+ /**
86
+ * @param provider the run's OWN provider connection (per-run, never a shared
87
+ * engine-level provider — see the per-run-connection requirement).
88
+ * @param runID CompanyIntegrationRun.ID this service governs.
89
+ * @param leaseMinutes lease length; callers pass
90
+ * max(DEFAULT_LEASE_MINUTES, options.MaxRuntimeMinutes ?? 0) so a
91
+ * long-batch override only ever EXTENDS protection.
92
+ */
93
+ constructor(provider: DatabaseProviderBase, runID: string, leaseMinutes?: number, contextUser?: UserInfo);
94
+ /** The CompanyIntegrationRun ID this service governs. */
95
+ get RunID(): string;
96
+ /** The opaque owner token minted for this execution. */
97
+ get OwnerToken(): string;
98
+ /** The fence token returned by the successful claim (null before claim). */
99
+ get FenceToken(): number | null;
100
+ /** Last lease expiry the DB confirmed for us (null before claim). */
101
+ get LeaseExpiresAt(): Date | null;
102
+ /** Lease length in minutes this service renews with. */
103
+ get LeaseMinutes(): number;
104
+ /**
105
+ * Atomically claim the run row. Returns true iff WE now own it (the sproc's
106
+ * single UPDATE succeeded because the row was unowned or its lease had
107
+ * expired). On success the DB-assigned FenceToken (bumped by the claim) and
108
+ * lease expiry are cached for renewals and boundary checks.
109
+ */
110
+ Claim(): Promise<boolean>;
111
+ /**
112
+ * Renew the lease (token+fence-checked). Zero rows back ⇒ ownership lost.
113
+ * Optionally piggybacks a progress snapshot onto the same write, and always
114
+ * surfaces the row's CancelRequestedAt so the heartbeat doubles as the
115
+ * cross-process cancel poll.
116
+ */
117
+ Renew(progressJSON?: string | null): Promise<RenewResult>;
118
+ /**
119
+ * Batch-boundary fence: a cheap SELECT of the ownership columns. The sync
120
+ * loop calls this BEFORE each batch's writes; if we no longer own the row,
121
+ * the caller must throw {@link RunOwnershipLostError} and stop writing.
122
+ * Deliberately a plain read (no lease extension) — renewal belongs to the
123
+ * heartbeat timer, and a boundary check must stay cheap enough to run
124
+ * every batch.
125
+ */
126
+ CheckBoundary(): Promise<BoundaryCheckResult>;
127
+ /**
128
+ * Terminal release: set the final status, clear OwnerToken/LeaseExpiresAt,
129
+ * stamp EndedAt if unset. Token- AND fence-checked — a stale holder (lease
130
+ * reclaimed) releasing late is a harmless no-op (returns false).
131
+ *
132
+ * The fence is sent for the same reason Renew() sends it: the owner token proves
133
+ * only that *some* context using this token owns the row, not that THIS context
134
+ * still does. Each instance mints its own token and claims once, so the two are
135
+ * equivalent today — but Claim() is re-callable and overwrites the fence, so
136
+ * passing it keeps the guarantee in the procedure rather than in call-site
137
+ * discipline.
138
+ */
139
+ Release(finalStatus: TerminalRunStatus): Promise<boolean>;
140
+ /**
141
+ * Start the background renewal timer at interval ≈ lease/3 (so a renewal
142
+ * must fail ~3 consecutive times before the lease can lapse). The timer
143
+ * body is best-effort and never throws; renewal failures surface through
144
+ * opts.onLost exactly once. Idempotent — restarting replaces the timer.
145
+ */
146
+ StartHeartbeat(opts?: HeartbeatOptions): void;
147
+ /** Stop the renewal timer (idempotent). */
148
+ StopHeartbeat(): void;
149
+ /**
150
+ * Sync the entity's in-memory ownership columns to the service's last-known
151
+ * authoritative values before ANY full-row `run.Save()`. The generated
152
+ * spUpdate writes every column from the entity's in-memory state, so a
153
+ * terminal Save without this sync would clobber the DB's live
154
+ * FenceToken/OwnerToken/LeaseExpiresAt with the stale values the entity was
155
+ * loaded with (typically pre-claim). Call this immediately before each
156
+ * Save on the run row; Release() then clears ownership atomically.
157
+ */
158
+ SyncEntityOwnershipFields(run: MJCompanyIntegrationRunEntity): void;
159
+ private lastProgressWriteMs;
160
+ /**
161
+ * Persist a progress snapshot to the run row's ProgressJSON (PR 1 item 4 —
162
+ * progress lives in the database; readers query the row). Ownership-guarded
163
+ * (WHERE OwnerToken AND FenceToken) so a reclaimed run can never overwrite the
164
+ * new owner's progress. Throttled internally (default once per 5s) so progress
165
+ * never becomes its own hot path; best-effort — a progress write failure must
166
+ * never fault the sync.
167
+ */
168
+ WriteProgress(progressJSON: string, minIntervalMs?: number): Promise<void>;
169
+ /** One heartbeat tick: renew, then fan out lost/cancel notifications once each. */
170
+ private heartbeatOnce;
171
+ /**
172
+ * Dialect-portable sproc call, preserving the MJ positional-parameter-array
173
+ * convention (same helper shape as ScheduledJobEngine.buildLockSprocCall).
174
+ */
175
+ private buildSprocCall;
176
+ }
177
+ //# sourceMappingURL=RunOwnershipService.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"RunOwnershipService.d.ts","sourceRoot":"","sources":["../src/RunOwnershipService.ts"],"names":[],"mappings":"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"}