Member Junction
    Preparing search index...

    Context passed to FetchChanges for incremental data retrieval

    interface FetchContext {
        AfterKeyValue?: string;
        BatchSize: number;
        CompanyIntegration: MJCompanyIntegrationEntity;
        ContextUser: UserInfo;
        CurrentCursor?: string;
        CurrentOffset?: number;
        CurrentPage?: number;
        DeadlineMs?: number;
        IsDiscoverySample?: boolean;
        MaxConcurrency?: number;
        ObjectName: string;
        RateLimitAcquire?: () => Promise<void>;
        RateLimitReport?: (throttledErr?: unknown) => void;
        RequestedSourceFields?: string[];
        SampleTargetRecords?: number;
        WatermarkValue: string;
    }
    Index

    Properties

    AfterKeyValue?: string

    KEYSET / seek resume position (plan.md §7): the last-seen value of the connector's StableOrderingKey. The connector fetches WHERE <key> > AfterKeyValue ORDER BY <key> so a mid-stream insert/delete cannot corrupt the scan position. Engine passes it on subsequent calls (and on restart-recovery). undefined/null on the first page.

    BatchSize: number

    Maximum number of records to fetch in a single batch

    CompanyIntegration: MJCompanyIntegrationEntity

    The company integration entity providing connection details

    ContextUser: UserInfo

    User context for authorization

    CurrentCursor?: string

    Current cursor for cursor-based pagination. Passed by engine on subsequent calls.

    CurrentOffset?: number

    Current offset for offset-based pagination. Passed by engine on subsequent calls.

    CurrentPage?: number

    Current page number for page-based pagination (1-based). Passed by engine on subsequent calls.

    DeadlineMs?: number
    IsDiscoverySample?: boolean

    SAMPLING, NOT SYNCING — and the wall-clock this call must not outlive.

    Discovery wants a corpus, not a corpus of everything: ~50 records is enough to infer columns, types, string widths and a provable primary key. DiscoverFieldsViaFetch already knows that and already computes a budget — but it hands that budget to the code CONSUMING the record stream, and the consumer only regains control BETWEEN FetchChanges calls. Nothing was ever passed to the connector itself, so a connector could not honour a budget even if it wanted to: it had no way to know it was being sampled rather than synced.

    That is survivable while one FetchChanges is one HTTP page — the consumer stops after 50 records and the gap never shows. It is NOT survivable for a parent-scoped object, where a single call fans out internally into one request per parent. There the consumer cannot interrupt anything, because control does not come back until every parent has been walked.

    Observed live 2026-08-12: a Totara discovery spent 28 minutes inside ONE FetchChanges call, walking every parent, and returned rows=0 — half an hour of correct, pointless work to collect a sample it could never have found there. A sampling operation had silently become an exhaustive one.

    So the intent now travels with the call. A connector that ignores these behaves exactly as before; one that fans out internally can stop early and return what it has.

    STOP ON RECORDS, NOT ON PARENTS. A child object only yields through its parents, so capping the number of parents visited would be wrong — if the first three courses have no enrolments you genuinely must keep walking to find fifty rows. SampleTargetRecords is therefore the primary stop and the walk should honour it the moment it is met, whichever parent it happens to be on. DeadlineMs is the BACKSTOP for the other case: parents that will never yield anything, where no record count can ever be reached and only the clock can end it.

    • IsDiscoverySample — this call exists to characterise the shape of the data, not to move it.
    • SampleTargetRecords — stop as soon as this many records have been collected. Enough is enough.
    • DeadlineMs — epoch ms after which the connector should stop and return what it has. A partial sample is the CORRECT result here: discovery infers from whatever it gets, and returning little beats half an hour of silence.
    MaxConcurrency?: number
    ObjectName: string

    External object name to fetch from

    RateLimitAcquire?: () => Promise<void>

    Adaptive rate-limit hooks (plan.md §7), supplied by the engine so a connector's INNER request loop (e.g. a second-layer/parent-iterated object that fires one request per parent) is governed by the SAME per-credential AIMD token bucket that paces the outer object level — instead of a fixed self-throttle that defeats concurrency and ignores 429 back-off. Optional + back-compat: a connector that ignores them behaves exactly as before; the engine omits them when unavailable.

    • RateLimitAcquire() — await one token from the adaptive bucket before each inner request.
    • RateLimitReport(err?) — feed the outcome back so the rate auto-tunes (clean → ramp up, 429 → back off).
    • MaxConcurrency — the engine's resolved in-flight cap for inner requests (>=1).
    RateLimitReport?: (throttledErr?: unknown) => void
    RequestedSourceFields?: string[]

    Optional list of source field names to request from the external API. When provided, the connector should limit the returned fields to this set.

    SampleTargetRecords?: number
    WatermarkValue: string

    Current watermark value for incremental fetch, or null for full fetch