@memberjunction/connector-id-window-scan 1.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,108 @@
1
+ import type { FetchWarning } from '@memberjunction/integration-engine';
2
+ /**
3
+ * ID-WINDOW SCAN — how to read an object whose "list" function is not a list function.
4
+ *
5
+ * A depressing number of vendor APIs expose an object that cannot actually be listed: the documented
6
+ * list call has no pagination params, or it is a SEARCH endpoint that the vendor's own docs warn will
7
+ * "be very slow or timeout" without narrow criteria. Asked for everything, it never answers inside the
8
+ * engine's `FetchChangesMs` budget, the batch is KILLED, and the object persists **zero records behind
9
+ * an otherwise-green run** — the worst failure shape there is, because it reads as "this tenant has no
10
+ * people" rather than as an error. (Found live on Totara/Moodle `core_user_get_users`; the same shape
11
+ * exists wherever a bulk reader takes an explicit key list but the list endpoint takes none.)
12
+ *
13
+ * The way out is to stop asking the question the server cannot answer. Nearly every such API also
14
+ * offers a BULK-BY-KEY reader — "give me the records for exactly these ids" — which is an indexed
15
+ * primary-key lookup rather than a search. This helper walks the object's numeric key in bounded
16
+ * windows through that reader, so **every request is bounded by construction**, and resumes across
17
+ * `FetchChanges` calls via the engine's keyset channel so a scan spans as many calls as it needs
18
+ * without ever re-reading from the top.
19
+ *
20
+ * Three properties are load-bearing, and each one was paid for by a live failure:
21
+ *
22
+ * 1. **The CALL bounds itself in time, not just each request.** Bounding requests alone is not enough —
23
+ * several bounded windows in one call can overrun the budget together and be killed with nothing,
24
+ * which is the original defect wearing a different hat. The scan carries `budgetMs` (under the
25
+ * engine's kill), returns what it scanned with its cursor, and says so via `ID_WINDOW_BUDGET_STOP`.
26
+ * 2. **One unreadable record costs one record, not the object.** Some vendors validate their own
27
+ * response per record, so a single bad row fails the whole call however many good rows it held.
28
+ * Since a failed window (correctly) is not an empty one, the cursor cannot advance and the scan
29
+ * re-requests the same window forever. Failed windows are therefore BISECTED to single ids; an id
30
+ * that fails alone is skipped with `ID_WINDOW_RECORD_SKIPPED` naming it, and the scan moves on.
31
+ * 3. **Coverage is never traded for speed.** Windows are contiguous and the cursor only ever advances
32
+ * over the prefix actually examined, so no id is stepped over unread. Stopping on the past-the-end
33
+ * heuristic ALWAYS emits `ID_WINDOW_SCAN_END` with the range covered, so a premature stop is visible
34
+ * in the run instead of silently truncating the object.
35
+ *
36
+ * The helper is deliberately vendor-agnostic: it knows nothing about HTTP, auth, or record shape. The
37
+ * caller supplies {@link IdWindowScanOptions.FetchWindow}, which turns a list of ids into raw records
38
+ * (and THROWS on any vendor or transport error — that is the signal bisection reads), and maps the raw
39
+ * records it gets back into `ExternalRecord`s itself.
40
+ */
41
+ /** Knobs, as read from an object's `Configuration.idWindowScan`. Every one has a working default. */
42
+ export interface IdWindowScanConfig {
43
+ /** Name of the numeric key being walked. Used in messages and to read the id off a raw record. Default `'id'`. */
44
+ field?: string;
45
+ /** Ids per request. Keep it small enough that a FULL BISECTION of one window still fits the budget. Default 25. */
46
+ windowSize?: number;
47
+ /** Windows attempted per `FetchChanges` call (run concurrently, folded in order). Default 2. */
48
+ windowsPerCall?: number;
49
+ /** Consecutive empty windows that mean "past the end of the table". Default 40. */
50
+ maxConsecutiveEmptyWindows?: number;
51
+ /**
52
+ * Wall-clock budget for the whole call. MUST be under the engine's `FetchChangesMs` (30000), because a
53
+ * call the engine kills persists NOTHING. Default 20000. Zero is meaningful and testable: "one window,
54
+ * then stop".
55
+ */
56
+ budgetMs?: number;
57
+ /** Bisection splits allowed per call, so one pathological window cannot monopolise it. Default 8. */
58
+ maxBisectSplitsPerCall?: number;
59
+ /**
60
+ * Consecutive unreadable ids after which the scan stops bisecting and reads id-at-a-time until one
61
+ * succeeds. Default 2. Live evidence: unreadable ids arrive in CONTIGUOUS BLOCKS (one bad profile field
62
+ * on a batch of accounts created together), so re-deriving the same bisection tree for each neighbour is
63
+ * pure waste — 235 requests to skip 57 records, observed. Raise it to bisect longer before giving up on
64
+ * bulk reads; there is no correctness difference either way, only cost.
65
+ */
66
+ singleStepAfterConsecutiveSkips?: number;
67
+ }
68
+ export interface IdWindowScanOptions {
69
+ /** Object name, used only in warning text. */
70
+ ObjectName: string;
71
+ /** Knobs (typically the object's `Configuration.idWindowScan`). */
72
+ Config?: IdWindowScanConfig;
73
+ /** The engine's keyset cursor from the previous call (`FetchContext.AfterKeyValue`). */
74
+ AfterKeyValue?: string | null;
75
+ /** `FetchContext.MaxConcurrency` — how many windows may be in flight at once. Default 1. */
76
+ MaxConcurrency?: number;
77
+ /** `FetchContext.RateLimitAcquire` — awaited before every request, including bisection sub-requests. */
78
+ RateLimitAcquire?: () => Promise<void>;
79
+ /** `FetchContext.RateLimitReport` — called after every response, and with the error on a rate-limit refusal. */
80
+ RateLimitReport?: (error?: unknown) => void;
81
+ /**
82
+ * Read exactly these ids. MUST THROW on any vendor or transport error — a thrown error is what triggers
83
+ * bisection, and an error swallowed into an empty array reads as "past the end of the table" instead.
84
+ * Rate-limit refusals should throw with a message matching 429 / "rate limit" / "Retry-After" so the scan
85
+ * propagates them for the engine's backoff rather than bisecting into the limiter.
86
+ */
87
+ FetchWindow: (ids: number[]) => Promise<Record<string, unknown>[]>;
88
+ /** How to read the id off a raw record. Defaults to `Number.parseInt(record[field])`. */
89
+ IdOf?: (record: Record<string, unknown>) => number;
90
+ /** Clock seam, for deterministic tests. Defaults to `Date.now`. */
91
+ Now?: () => number;
92
+ }
93
+ export interface IdWindowScanResult {
94
+ /** RAW records, in id order. The caller maps these to `ExternalRecord`s. */
95
+ Records: Record<string, unknown>[];
96
+ /** False only when the past-the-end heuristic fired — i.e. the scan is complete. */
97
+ HasMore: boolean;
98
+ /** Opaque cursor to hand back as `FetchContext.AfterKeyValue`. Absent once `HasMore` is false. */
99
+ NextAfterKeyValue?: string;
100
+ /** Diagnostics for the engine's run artifact. Never empty-but-present; absent when there are none. */
101
+ Warnings?: FetchWarning[];
102
+ }
103
+ /**
104
+ * Run one `FetchChanges` worth of id-window scanning. See the module header for the why.
105
+ *
106
+ * @returns raw records plus the cursor to resume from; `HasMore:false` only when the scan is genuinely done.
107
+ */
108
+ export declare function runIdWindowScan(options: IdWindowScanOptions): Promise<IdWindowScanResult>;
package/dist/index.js ADDED
@@ -0,0 +1,332 @@
1
+ class IdWindowDeadlineError extends Error {
2
+ constructor() {
3
+ super('id-window scan reached its fetch budget');
4
+ this.name = 'IdWindowDeadlineError';
5
+ }
6
+ }
7
+ /** Bounded-concurrency map. Kept local so the package stays dependency-free. */
8
+ async function runBounded(items, limit, worker) {
9
+ const queue = [...items];
10
+ const lanes = Array.from({ length: Math.max(1, Math.min(limit, queue.length)) }, async () => {
11
+ for (;;) {
12
+ const next = queue.shift();
13
+ if (next === undefined)
14
+ return;
15
+ await worker(next);
16
+ }
17
+ });
18
+ await Promise.all(lanes);
19
+ }
20
+ /**
21
+ * The cursor is `"<nextStartId>|<consecutiveEmptyWindows>|<highestIdSeen>|<consecutiveSkips>"`.
22
+ *
23
+ * `highestIdSeen` rides the cursor rather than being tracked per call because the call that ENDS a scan is,
24
+ * by construction, the one whose windows were all empty — so a per-call maximum reports 0 on exactly the
25
+ * warning that needs it (observed: `ID_WINDOW_SCAN_END` claiming `highestIdSeen: 0` after landing 24,682
26
+ * users). `consecutiveSkips` rides it for the same reason: a poison block is far longer than one call's
27
+ * couple of windows, so a per-call counter would forget it at every call boundary and pay for the first
28
+ * bisection again and again. Shorter cursors from an older build still parse; the missing parts start at 0.
29
+ */
30
+ function parseCursor(raw) {
31
+ const out = { StartId: 1, EmptyRun: 0, HighestSeen: 0, SkipRun: 0 };
32
+ if (!raw)
33
+ return out;
34
+ const [rawStart, rawEmpty, rawHighest, rawSkips] = String(raw).split('|');
35
+ const start = Number.parseInt(rawStart ?? '', 10);
36
+ const empty = Number.parseInt(rawEmpty ?? '', 10);
37
+ const highest = Number.parseInt(rawHighest ?? '', 10);
38
+ const skips = Number.parseInt(rawSkips ?? '', 10);
39
+ if (Number.isFinite(start) && start > 0)
40
+ out.StartId = start;
41
+ if (Number.isFinite(empty) && empty >= 0)
42
+ out.EmptyRun = empty;
43
+ if (Number.isFinite(highest) && highest >= 0)
44
+ out.HighestSeen = highest;
45
+ if (Number.isFinite(skips) && skips >= 0)
46
+ out.SkipRun = skips;
47
+ return out;
48
+ }
49
+ /**
50
+ * Run one `FetchChanges` worth of id-window scanning. See the module header for the why.
51
+ *
52
+ * @returns raw records plus the cursor to resume from; `HasMore:false` only when the scan is genuinely done.
53
+ */
54
+ export async function runIdWindowScan(options) {
55
+ const objectName = options.ObjectName;
56
+ const cfg = options.Config ?? {};
57
+ const field = cfg.field ?? 'id';
58
+ const windowSize = positive(cfg.windowSize) ?? 25;
59
+ const windowsPerCall = positive(cfg.windowsPerCall) ?? 2;
60
+ const maxEmpty = positive(cfg.maxConsecutiveEmptyWindows) ?? 40;
61
+ const budgetMs = nonNegative(cfg.budgetMs) ?? 20000;
62
+ const now = options.Now ?? Date.now;
63
+ const idOf = options.IdOf ?? ((record) => Number.parseInt(String(record[field] ?? ''), 10));
64
+ const startedAt = now();
65
+ const outOfTime = () => now() - startedAt >= budgetMs;
66
+ const cursor = parseCursor(options.AfterKeyValue);
67
+ const startId = cursor.StartId;
68
+ let emptyRun = cursor.EmptyRun;
69
+ let highestSeen = cursor.HighestSeen;
70
+ const warnings = [];
71
+ const windows = Array.from({ length: windowsPerCall }, (_unused, w) => startId + w * windowSize);
72
+ const perWindow = new Map();
73
+ // Ids the vendor could not return AT ALL. Recorded so the fold can tell "this window held nothing" from
74
+ // "this window held records we were refused" — only the former is evidence of being past the end.
75
+ const skippedIds = new Set();
76
+ const bisectBudget = { remaining: positive(cfg.maxBisectSplitsPerCall) ?? 8 };
77
+ // THE PROGRESS GUARANTEE. The deadline (and the bisect budget) are suspended until at least one id has
78
+ // been EXAMINED — read, or proven unreadable. Not "one request issued": a call whose single request hit a
79
+ // poisoned window would resolve nothing, return empty with an unchanged cursor, and be the same call
80
+ // forever. Once one id is resolved the cursor can advance, so everything after that obeys the deadline.
81
+ const resolved = { any: false };
82
+ const deadlineHit = () => outOfTime() && resolved.any;
83
+ // Carried across calls on the cursor — a poison block outlives any one call's windows.
84
+ const skipRun = { count: cursor.SkipRun };
85
+ const scanCtx = {
86
+ ObjectName: objectName,
87
+ Field: field,
88
+ FetchWindow: options.FetchWindow,
89
+ RateLimitAcquire: options.RateLimitAcquire,
90
+ RateLimitReport: options.RateLimitReport,
91
+ Warnings: warnings,
92
+ SkippedIds: skippedIds,
93
+ BisectBudget: bisectBudget,
94
+ Resolved: resolved,
95
+ SkipRun: skipRun,
96
+ SingleStepAfter: positive(cfg.singleStepAfterConsecutiveSkips) ?? 2,
97
+ SingleStepAnnounced: { done: false },
98
+ };
99
+ await runBounded(windows, Math.max(1, options.MaxConcurrency ?? 1), async (windowStart) => {
100
+ // ONLY the first window carries the progress exemption. The windows run concurrently, so if they all
101
+ // ignored the deadline while nothing was resolved, an unreadable range would put every lane into its
102
+ // own bisection at once — several times the work the budget allows, and the call is killed with
103
+ // nothing again. The cursor only ever needs the FIRST window to advance.
104
+ const gate = windowStart === startId ? deadlineHit : outOfTime;
105
+ if (gate())
106
+ return; // never started → left unset → the fold resumes from here
107
+ perWindow.set(windowStart, await fetchWindowResilient(scanCtx, windowStart, windowSize, gate));
108
+ });
109
+ const out = [];
110
+ let nextStartId = startId;
111
+ let stopped = false;
112
+ for (const windowStart of windows) {
113
+ const result = perWindow.get(windowStart);
114
+ if (result === undefined)
115
+ break; // window never started — resume here next call
116
+ // Advance ONLY over the contiguous prefix actually resolved. A window cut short mid-bisection still
117
+ // contributes everything it read; the scan resumes at the first id it did not reach, so no id is ever
118
+ // stepped over unexamined and no completed work is thrown away.
119
+ nextStartId = result.ResolvedThrough + 1;
120
+ const fullyResolved = result.ResolvedThrough >= windowStart + windowSize - 1;
121
+ const refusedHere = [...skippedIds].some((id) => id >= windowStart && id < windowStart + windowSize);
122
+ if (result.Records.length === 0) {
123
+ if (!refusedHere && fullyResolved)
124
+ emptyRun++;
125
+ }
126
+ else {
127
+ emptyRun = 0;
128
+ for (const raw of result.Records) {
129
+ const idVal = idOf(raw);
130
+ if (Number.isFinite(idVal) && idVal > highestSeen)
131
+ highestSeen = idVal;
132
+ out.push(raw);
133
+ }
134
+ }
135
+ if (emptyRun >= maxEmpty) {
136
+ stopped = true;
137
+ break;
138
+ }
139
+ // Partially resolved → everything after it in this call is unexamined. Stop folding so the cursor stays
140
+ // at the first unread id; later windows this call may have completed, but honouring them would step
141
+ // over the gap.
142
+ if (!fullyResolved)
143
+ break;
144
+ }
145
+ // Ran out of budget before working through the windows this call planned. Not an error and not the end of
146
+ // the scan — say so explicitly, so a short batch is never mistaken for a thin id range.
147
+ if (!stopped && nextStartId < startId + windowsPerCall * windowSize && outOfTime()) {
148
+ warnings.push({
149
+ Code: 'ID_WINDOW_BUDGET_STOP',
150
+ Message: `"${objectName}": stopped this batch at the ${budgetMs}ms fetch budget having scanned ` +
151
+ `${field} ${startId}-${nextStartId - 1}; the rest resumes on the next call.`,
152
+ Data: { object: objectName, scannedThrough: nextStartId - 1, budgetMs, windowSize, windowsPerCall },
153
+ });
154
+ }
155
+ if (stopped) {
156
+ warnings.push({
157
+ Code: 'ID_WINDOW_SCAN_END',
158
+ Message: `"${objectName}": stopped after ${emptyRun} consecutive empty ${field} windows ` +
159
+ `(${emptyRun * windowSize} ids with no records). Scanned ${field} 1-${nextStartId - 1}, ` +
160
+ `highest ${field} seen ${highestSeen}. If this site has id gaps wider than that, raise ` +
161
+ `Configuration.idWindowScan.maxConsecutiveEmptyWindows.`,
162
+ Data: {
163
+ object: objectName,
164
+ scannedThrough: nextStartId - 1,
165
+ highestIdSeen: highestSeen,
166
+ windowSize,
167
+ maxConsecutiveEmptyWindows: maxEmpty,
168
+ },
169
+ });
170
+ }
171
+ return {
172
+ Records: out,
173
+ HasMore: !stopped,
174
+ NextAfterKeyValue: stopped ? undefined : `${nextStartId}|${emptyRun}|${highestSeen}|${skipRun.count}`,
175
+ Warnings: warnings.length ? warnings : undefined,
176
+ };
177
+ }
178
+ /** One request for a contiguous id window. Throws on any transport or in-band vendor error. */
179
+ async function fetchWindowRequest(ctx, windowStart, windowSize, outOfTime) {
180
+ // The deadline is checked on BOTH sides of the rate-limit acquire. Checking only before issuing is not
181
+ // enough: acquiring a token can block for an unbounded time, so a request cleared at 19s can still be in
182
+ // flight past the engine's 30s kill — which is how a bisecting call overran its budget and was killed with
183
+ // nothing to show, the exact failure the budget exists to prevent.
184
+ if (outOfTime())
185
+ throw new IdWindowDeadlineError();
186
+ if (ctx.RateLimitAcquire)
187
+ await ctx.RateLimitAcquire();
188
+ if (outOfTime())
189
+ throw new IdWindowDeadlineError();
190
+ const ids = Array.from({ length: windowSize }, (_unused, i) => windowStart + i);
191
+ const records = await ctx.FetchWindow(ids);
192
+ ctx.RateLimitReport?.();
193
+ // Marked resolved only AFTER a successful read. A window that failed has examined nothing; marking it
194
+ // resolved would re-arm the deadline and strand the bisection that was about to isolate the bad id.
195
+ ctx.Resolved.any = true;
196
+ // A read that worked is the end of the poison block, whatever its size — one good id is enough to
197
+ // justify going back to bulk requests.
198
+ ctx.SkipRun.count = 0;
199
+ return records;
200
+ }
201
+ /**
202
+ * Fetch one id window, ISOLATING the ids the vendor refuses instead of losing the window to them.
203
+ *
204
+ * Returns the records read plus `ResolvedThrough` — the last id of the CONTIGUOUS PREFIX actually examined.
205
+ * A window cut short by the deadline or the bisect budget reports the prefix it got through, so the caller
206
+ * resumes at the first unread id: partial progress is kept, and no id is ever stepped over unexamined.
207
+ */
208
+ async function fetchWindowResilient(ctx, windowStart, windowSize, outOfTime) {
209
+ const nothingRead = { Records: [], ResolvedThrough: windowStart - 1 };
210
+ // Already inside a run of unreadable ids → do not pay for a bulk request that is about to fail and a
211
+ // bisection that is about to re-derive what the last window already proved. See fetchWindowSingleStep.
212
+ if (windowSize > 1 && ctx.SkipRun.count >= ctx.SingleStepAfter) {
213
+ return await fetchWindowSingleStep(ctx, windowStart, windowSize, outOfTime);
214
+ }
215
+ try {
216
+ const records = await fetchWindowRequest(ctx, windowStart, windowSize, outOfTime);
217
+ return { Records: records, ResolvedThrough: windowStart + windowSize - 1 };
218
+ }
219
+ catch (e) {
220
+ if (e instanceof IdWindowDeadlineError)
221
+ return nothingRead; // out of budget → resume here next call
222
+ const msg = e instanceof Error ? e.message : String(e);
223
+ if (/429|rate.?limit|Retry-After/i.test(msg)) {
224
+ ctx.RateLimitReport?.(e); // rate limit → propagate for the engine's backoff, never bisect into it
225
+ throw e;
226
+ }
227
+ if (windowSize === 1) {
228
+ // Single id. Retry once: a record that fails the vendor's own validation fails every time, a blip
229
+ // does not — so one retry is what separates "bad record" from "bad moment".
230
+ try {
231
+ const records = await fetchWindowRequest(ctx, windowStart, 1, outOfTime);
232
+ return { Records: records, ResolvedThrough: windowStart };
233
+ }
234
+ catch (retryErr) {
235
+ if (retryErr instanceof IdWindowDeadlineError)
236
+ return nothingRead;
237
+ const retryMsg = retryErr instanceof Error ? retryErr.message : String(retryErr);
238
+ ctx.SkippedIds.add(windowStart);
239
+ ctx.Resolved.any = true; // an id proven unreadable is still an id examined
240
+ ctx.SkipRun.count++;
241
+ ctx.Warnings.push({
242
+ Code: 'ID_WINDOW_RECORD_SKIPPED',
243
+ Message: `"${ctx.ObjectName}" ${ctx.Field}=${windowStart} could not be read and was SKIPPED — the ` +
244
+ `rest of the range synced normally. The vendor rejects its own response for this record ` +
245
+ `(typically one field that fails the vendor's return validation): ${retryMsg}`,
246
+ Data: { object: ctx.ObjectName, field: ctx.Field, id: windowStart, error: retryMsg },
247
+ });
248
+ return { Records: [], ResolvedThrough: windowStart }; // examined, unreadable — move on
249
+ }
250
+ }
251
+ ctx.Warnings.push({
252
+ Code: 'ID_WINDOW_FETCH_ERROR',
253
+ Message: `"${ctx.ObjectName}" ${ctx.Field} window ${windowStart}-${windowStart + windowSize - 1}: ${msg} ` +
254
+ `— bisecting to isolate the unreadable ${ctx.Field}(s).`,
255
+ Data: { object: ctx.ObjectName, windowStart, windowSize },
256
+ });
257
+ // Bisect. The halves are sequential and the result is the CONTIGUOUS prefix: if the left half stops
258
+ // short the right half is never attributed, because ids in the gap would otherwise be stepped over as
259
+ // if they had been examined.
260
+ // While nothing has been resolved yet the descent continues regardless of budget — but only down the
261
+ // LEFT spine (the `return left` below fires immediately, since a left half that resolved nothing is
262
+ // short). That isolates the first id in ~log2(windowSize) requests, which is what makes the progress
263
+ // guarantee cheap even against a wholly unreadable window.
264
+ if (ctx.Resolved.any && (ctx.BisectBudget.remaining <= 0 || outOfTime()))
265
+ return nothingRead;
266
+ ctx.BisectBudget.remaining--;
267
+ const half = Math.floor(windowSize / 2);
268
+ const left = await fetchWindowResilient(ctx, windowStart, half, outOfTime);
269
+ if (left.ResolvedThrough < windowStart + half - 1)
270
+ return left;
271
+ if (ctx.BisectBudget.remaining <= 0 || outOfTime())
272
+ return left; // keep the prefix rather than lose it
273
+ const right = await fetchWindowResilient(ctx, windowStart + half, windowSize - half, outOfTime);
274
+ return {
275
+ Records: [...left.Records, ...right.Records],
276
+ ResolvedThrough: Math.max(left.ResolvedThrough, right.ResolvedThrough),
277
+ };
278
+ }
279
+ }
280
+ /**
281
+ * Read a window ONE ID AT A TIME, for as long as the ids keep proving unreadable.
282
+ *
283
+ * Unreadable ids are not scattered — live evidence is a single stray plus an unbroken block of 53. Bisection
284
+ * isolates the first of those correctly, but it starts over for the next id, and the next: the descent
285
+ * 25→12→6→3→2→1 was paid 57 times to skip 57 records, 235 requests in all. Once a run of ids has already been
286
+ * proven unreadable, the bulk request is a request known to fail and the descent is a rediscovery of what the
287
+ * previous window just established, so both are skipped.
288
+ *
289
+ * This is a COST optimisation with no bearing on coverage: every id is still examined individually, still
290
+ * skipped with its own `ID_WINDOW_RECORD_SKIPPED`, and `ResolvedThrough` is still the contiguous prefix
291
+ * actually read. The first id that SUCCEEDS clears the run, and the rest of the window is fetched in bulk
292
+ * again — the fast path is the normal path the moment the block ends.
293
+ */
294
+ async function fetchWindowSingleStep(ctx, windowStart, windowSize, outOfTime) {
295
+ if (!ctx.SingleStepAnnounced.done) {
296
+ ctx.SingleStepAnnounced.done = true;
297
+ ctx.Warnings.push({
298
+ Code: 'ID_WINDOW_SINGLE_STEP',
299
+ Message: `"${ctx.ObjectName}": ${ctx.SkipRun.count} consecutive unreadable ${ctx.Field}s, so this range is ` +
300
+ `being read one ${ctx.Field} at a time instead of bisected per window. Coverage is unchanged — ` +
301
+ `every ${ctx.Field} is still tried and every skip still reported. Bulk reads resume at the first ` +
302
+ `${ctx.Field} that succeeds.`,
303
+ Data: { object: ctx.ObjectName, field: ctx.Field, windowStart, windowSize, consecutiveSkips: ctx.SkipRun.count },
304
+ });
305
+ }
306
+ const records = [];
307
+ let resolvedThrough = windowStart - 1;
308
+ for (let id = windowStart; id < windowStart + windowSize; id++) {
309
+ if (ctx.SkipRun.count < ctx.SingleStepAfter) {
310
+ // An id came back: the block is over. Read everything left in this window as one request, which
311
+ // re-enters the ordinary path (the guard above no longer fires), so there is no recursion risk.
312
+ const rest = await fetchWindowResilient(ctx, id, windowStart + windowSize - id, outOfTime);
313
+ return {
314
+ Records: [...records, ...rest.Records],
315
+ ResolvedThrough: Math.max(resolvedThrough, rest.ResolvedThrough),
316
+ };
317
+ }
318
+ const one = await fetchWindowResilient(ctx, id, 1, outOfTime);
319
+ if (one.ResolvedThrough < id)
320
+ break; // deadline mid-window → keep the prefix, resume here next call
321
+ records.push(...one.Records);
322
+ resolvedThrough = id;
323
+ }
324
+ return { Records: records, ResolvedThrough: resolvedThrough };
325
+ }
326
+ function positive(value) {
327
+ return typeof value === 'number' && Number.isFinite(value) && value > 0 ? Math.floor(value) : undefined;
328
+ }
329
+ function nonNegative(value) {
330
+ return typeof value === 'number' && Number.isFinite(value) && value >= 0 ? Math.floor(value) : undefined;
331
+ }
332
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"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"}
package/package.json ADDED
@@ -0,0 +1,31 @@
1
+ {
2
+ "name": "@memberjunction/connector-id-window-scan",
3
+ "version": "1.1.0",
4
+ "description": "Pure helper that reads an object the vendor will not bulk-list, by walking its numeric key in bounded windows. Budget-aware, resumable, and it isolates records the vendor refuses instead of losing the object to them. One function, no class, no base — connectors import it; they do NOT extend anything new.",
5
+ "type": "module",
6
+ "main": "dist/index.js",
7
+ "types": "dist/index.d.ts",
8
+ "files": [
9
+ "/dist"
10
+ ],
11
+ "scripts": {
12
+ "build": "tsc",
13
+ "test": "vitest run --passWithNoTests"
14
+ },
15
+ "author": "MemberJunction.com",
16
+ "license": "ISC",
17
+ "peerDependencies": {
18
+ "@memberjunction/integration-engine": ">=5.42.0 <6.0.0"
19
+ },
20
+ "dependencies": {},
21
+ "devDependencies": {
22
+ "@types/node": "24.10.11",
23
+ "typescript": "^5.9.3",
24
+ "vitest": "^4.0.18",
25
+ "@memberjunction/integration-engine": "^5.42.0"
26
+ },
27
+ "repository": {
28
+ "type": "git",
29
+ "url": "https://github.com/MemberJunction/Integrations"
30
+ }
31
+ }