@pattern-stack/codegen 0.12.1 → 0.13.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/CHANGELOG.md +102 -0
- package/README.md +44 -0
- package/dist/runtime/subsystems/index.d.ts +6 -2
- package/dist/runtime/subsystems/index.js +171 -1
- package/dist/runtime/subsystems/index.js.map +1 -1
- package/dist/runtime/subsystems/integration/detection-config.schema.d.ts +110 -1
- package/dist/runtime/subsystems/integration/detection-config.schema.js +25 -2
- package/dist/runtime/subsystems/integration/detection-config.schema.js.map +1 -1
- package/dist/runtime/subsystems/integration/incremental-read.d.ts +221 -0
- package/dist/runtime/subsystems/integration/incremental-read.js +146 -0
- package/dist/runtime/subsystems/integration/incremental-read.js.map +1 -0
- package/dist/runtime/subsystems/integration/index.d.ts +2 -1
- package/dist/runtime/subsystems/integration/index.js +169 -2
- package/dist/runtime/subsystems/integration/index.js.map +1 -1
- package/dist/runtime/subsystems/jobs/job-worker.module.d.ts +1 -1
- package/dist/src/cli/index.js +704 -93
- package/dist/src/cli/index.js.map +1 -1
- package/dist/src/index.d.ts +103 -15
- package/dist/src/index.js +54 -36
- package/dist/src/index.js.map +1 -1
- package/package.json +1 -1
- package/runtime/subsystems/index.ts +34 -0
- package/runtime/subsystems/integration/detection-config.schema.ts +55 -0
- package/runtime/subsystems/integration/incremental-read.ts +345 -0
- package/runtime/subsystems/integration/index.ts +15 -0
- package/templates/entity/new/clean-lite-ps/prompt-extension.js +2 -2
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../../../../runtime/subsystems/integration/incremental-read.ts"],"sourcesContent":["/**\n * Integration subsystem — `IncrementalRead<T, F>` + `RandomRead<T>` capability\n * and the providing `IncrementalReadBase<T, F, M>` (RFC-0003 R1).\n *\n * The universal read primitive. Where `IChangeSource.listChanges` is the\n * *transport* contract (stream `Change<T>`, orchestrator owns cursor lifecycle),\n * this base owns *how the body that produces those changes is written* — the\n * level the bare `changeSources = {}` author-seam left unstructured.\n *\n * The read decomposes into two composable verbs the adapter supplies:\n *\n * - `enumerate(mode, filter) → AsyncIterable<Ref<M>[]>` — the cheap delta /\n * backfill walk; streams pages of lightweight refs (id + per-ref cursor +\n * filterable metadata). LAZY: pull-driven so hydrate backpressures it.\n * - `hydrate(ids) → Map<id, raw>` — the expensive fetch-by-id, batched; where\n * bounded concurrency / a vendor `/batch` endpoint lives. Keyed and\n * miss-tolerant (a mid-run 404 cannot shift alignment).\n * - `toCanonical(raw) → T | null` — provider payload → canonical record.\n *\n * The base PROVIDES the orchestration: drain enumerate, **filter before\n * hydrate** (structural — an adapter physically cannot hydrate-then-discard),\n * keyed pairing, per-ref cursor emission, and the `IChangeSource.listChanges`\n * adaptation. It also provides `RandomRead.get()` for free as\n * `toCanonical ∘ hydrate([id])` — so every incremental adapter is a\n * single-record reader (the \"list cheaply, fill on click\" query-surface need)\n * without extra code.\n *\n * The shape generalizes dealbrain's proven HubSpot `listSince` (streams, pushes\n * the filter server-side, carries a per-record cursor) to vendors whose list\n * returns id-stubs (Gmail) or nested resources (Meet). Calendar-style\n * full-object lists override `hydrate` as a passthrough.\n *\n * See RFC-0003 (Track D round-3), ADR-033 (`detection:` config), and\n * `poll-change-source.ts` (the sibling primitive this composes beside).\n */\n\nimport type {\n Change,\n ChangeSource,\n IChangeSource,\n IntegrationSubscriptionView,\n} from './integration-change-source.protocol';\n\n// ============================================================================\n// Capability shapes\n// ============================================================================\n\n/**\n * How a read walks the upstream. Modes are values, not verbs (swe-brain\n * ADR-0003: mode ≠ capability) — one `read()` verb dispatches on these.\n *\n * - `delta` — incremental walk from a persisted cursor.\n * - `full` — cursorless backfill (optionally bounded by `since`).\n * - `reconcile` — gap-repair: re-fetch a known id set the cursor skipped\n * (the repair pass for the silent-tail-skip + #414-style\n * multi-provider divergence).\n */\nexport type ReadMode =\n | { readonly kind: 'delta'; readonly cursor: unknown }\n | { readonly kind: 'full'; readonly since?: Date }\n | { readonly kind: 'reconcile'; readonly knownIds: readonly string[] };\n\n/**\n * A cheap ref from the enumerate pass: identity + per-ref cursor + metadata to\n * filter or display on. `cursor` is the position AS OF this ref — see\n * `IncrementalReadBase.cursorDivisible` (R2) for when it may be checkpointed\n * mid-walk versus withheld until a safe boundary.\n */\nexport interface Ref<M = Record<string, unknown>> {\n readonly externalId: string;\n readonly cursor: unknown;\n readonly meta: M;\n}\n\n/** A read request: the mode, an optional adapter-typed filter, and page size. */\nexport interface ReadRequest<F = unknown> {\n readonly mode: ReadMode;\n readonly filter?: F;\n readonly pageSize?: number;\n}\n\n/**\n * The `read()`-side envelope: canonical record + the raw vendor payload it came\n * from + the originating external id + the per-ref cursor.\n *\n * Distinct from the runtime's transport envelope `Change<T>`\n * (operation/externalId/cursor/source). The relationship is one-directional:\n * `listChanges()` adapts `read()` → `Change<T>` (dropping `raw`, stamping\n * `operation`). `read()` keeps `raw` and `externalId` so a query surface can\n * re-project without a second fetch.\n */\nexport interface SourcedRecord<T> {\n readonly externalId: string;\n readonly record: T;\n readonly raw: unknown;\n readonly cursor: unknown;\n}\n\n/**\n * The universal read capability — one public verb that streams. Filtering,\n * hydration, and cursor emission are the providing base's concern.\n */\nexport interface IncrementalRead<T, F = unknown> {\n read(req: ReadRequest<F>): AsyncIterable<SourcedRecord<T>>;\n}\n\n/**\n * Single-record read by external id — the \"fill on click\" atom. Provided for\n * free by `IncrementalReadBase` (composes `hydrate` + `toCanonical`); declared\n * as its own capability so consumers can depend on it without the streaming\n * surface.\n */\nexport interface RandomRead<T> {\n get(id: string): Promise<T | null>;\n}\n\n// ============================================================================\n// Bounded-parallel map helper\n// ============================================================================\n\n/**\n * Map `ids` through `fn` with at most `limit` concurrent in-flight calls,\n * collecting results keyed by id. The workhorse for writing a batched\n * `hydrate` over a single-id fetch without serial N+1 latency.\n */\nexport async function mapConcurrent<R>(\n ids: readonly string[],\n fn: (id: string) => Promise<R>,\n limit: number,\n): Promise<Map<string, R>> {\n const out = new Map<string, R>();\n if (ids.length === 0) return out;\n const width = Math.max(1, Math.min(limit, ids.length));\n let next = 0;\n const worker = async (): Promise<void> => {\n while (next < ids.length) {\n const idx = next++;\n const id = ids[idx]!;\n out.set(id, await fn(id));\n }\n };\n await Promise.all(Array.from({ length: width }, worker));\n return out;\n}\n\n// ============================================================================\n// IncrementalReadBase\n// ============================================================================\n\n/**\n * Providing base for the read capability. A subclass fills exactly three vendor\n * methods — `enumerate`, `hydrate`, `toCanonical` — and gets a streaming,\n * filter-before-hydrate, miss-tolerant `IncrementalRead<T, F>` +\n * `IChangeSource<T>` + `RandomRead<T>`.\n *\n * Type params: `T` canonical record, `F` adapter-typed filter, `M` per-ref\n * metadata (defaults to an untyped bag — surface packages supply a domain `M`).\n */\nexport abstract class IncrementalReadBase<T, F = unknown, M = Record<string, unknown>>\n implements IncrementalRead<T, F>, IChangeSource<T>, RandomRead<T>\n{\n /** Human label for run logs — e.g. `'google-mail-email'`. */\n abstract readonly label: string;\n\n /**\n * Whether the vendor takes the request predicate server-side. Declared, not\n * enforced here — surfaced into the emission manifest (R3) so the falsifier\n * suite (R4) can record which adapters filter post-hydrate. `false` is the\n * honest floor (e.g. Gmail without `q=`), handled via `matchesRecord`.\n */\n protected readonly filterPushdown: boolean = false;\n\n /** Max concurrent in-flight calls for a `mapConcurrent`-built `hydrate`. */\n protected readonly hydrateConcurrency: number = 10;\n\n /** `Change<T>.source` provenance stamped by `listChanges`. */\n protected readonly changeSource: ChangeSource = 'poll';\n\n /**\n * Whether this source's cursor strategy is divisible (RFC-0003 §3). When\n * `true` (default — sortable watermarks like `systemModstamp`/`timestamp`/\n * `replayId`), `listChanges` emits each record's per-ref cursor, so the\n * orchestrator may checkpoint mid-walk and a crash resumes from the last\n * delivered ref.\n *\n * When `false` (atomic opaque tokens — Gmail `historyId`, Calendar\n * `syncToken`), `listChanges` WITHHOLDS per-ref cursors and emits the\n * end-of-walk token only on the final record, so the orchestrator's\n * persist-last-yielded lifecycle can never persist an unresumable mid-walk\n * token. The cost is blast-radius: an interrupted atomic run resumes\n * all-or-nothing from the prior persisted token. For atomic *backfills* that\n * radius is the whole enumerate walk — bound it with `ReadRequest.pageSize`\n * (smaller pages ⇒ shorter walks per run). Per-page atomic checkpointing is a\n * future refinement; R2 gates at end-of-walk.\n *\n * Codegen (R3) sets this from the strategy kind via `isDivisibleCursor`.\n */\n protected readonly cursorDivisible: boolean = true;\n\n // ---- SUPPLIED by the adapter (the irreducible vendor seam) ----\n\n /**\n * The cheap walk. Streams pages of refs; LAZY so `hydrate` backpressures it\n * (one page hydrated before the next is pulled). Mode-dispatch lives here:\n * `delta` resumes from `mode.cursor`, `full` walks from the top, `reconcile`\n * re-fetches `mode.knownIds`.\n *\n * `pageSize` (from `ReadRequest`) is the adapter's requested vendor page size\n * — also the atomic-cursor backfill blast-radius bound (§ `cursorDivisible`).\n * Honor it as a hint; vendors that cap page size clamp it.\n */\n protected abstract enumerate(\n mode: ReadMode,\n filter?: F,\n pageSize?: number,\n ): AsyncIterable<Ref<M>[]>;\n\n /**\n * Fetch raw payloads for `ids`, keyed by id. MUST be miss-tolerant: omit (or\n * map to `null`) any id that 404s mid-run rather than throwing or shifting\n * alignment. Write it over `mapConcurrent(ids, (id) => this.fetchOne(id),\n * this.hydrateConcurrency)`; override with a real `/batch` call or a\n * passthrough (full-object list) where the vendor allows.\n */\n protected abstract hydrate(ids: string[]): Promise<Map<string, unknown>>;\n\n /** Provider payload → canonical record. Return `null` to drop a record. */\n protected abstract toCanonical(raw: unknown): T | null;\n\n // ---- Optional filter hooks — exactly one is live per `filterPushdown` ----\n\n /** Pre-hydrate predicate over the cheap ref (preferred — avoids hydration). */\n protected matchesRef(_ref: Ref<M>, _filter?: F): boolean {\n return true;\n }\n\n /** Post-hydrate predicate over the canonical record (the no-pushdown floor). */\n protected matchesRecord(_record: T, _filter?: F): boolean {\n return true;\n }\n\n /**\n * Resolve the filter for a subscription when adapting to `listChanges`\n * (which has no filter argument). Defaults to none; codegen wiring (R3)\n * overrides this to thread `DetectionConfig.filters`.\n */\n protected filterFor(_subscription: IntegrationSubscriptionView): F | undefined {\n return undefined;\n }\n\n // ---- PROVIDED by the base ----\n\n /**\n * Stream canonical records for a request. Filter is applied BEFORE hydrate\n * (structural: a kept ref is hydrated, a rejected one never is), so an\n * adapter cannot hydrate-then-discard. A hydrate miss (deleted mid-run) is\n * skipped, never fabricated.\n */\n async *read(req: ReadRequest<F>): AsyncIterable<SourcedRecord<T>> {\n for await (const refPage of this.enumerate(req.mode, req.filter, req.pageSize)) {\n const kept = refPage.filter((ref) => this.matchesRef(ref, req.filter));\n if (kept.length === 0) continue;\n const raws = await this.hydrate(kept.map((ref) => ref.externalId));\n for (const ref of kept) {\n const raw = raws.get(ref.externalId);\n if (raw === undefined || raw === null) continue; // deleted mid-run → skip\n const record = this.toCanonical(raw);\n if (record !== null && this.matchesRecord(record, req.filter)) {\n yield { externalId: ref.externalId, record, raw, cursor: ref.cursor };\n }\n }\n }\n }\n\n /**\n * `RandomRead<T>` — single-record read, provided for free as\n * `toCanonical ∘ hydrate([id])`. Reuses the adapter's batched fetch + miss\n * tolerance; returns `null` for a missing or undecodable record.\n */\n async get(id: string): Promise<T | null> {\n const raws = await this.hydrate([id]);\n const raw = raws.get(id);\n if (raw === undefined || raw === null) return null;\n return this.toCanonical(raw);\n }\n\n /**\n * `IChangeSource<T>` adaptation. Maps the orchestrator's by-value cursor to a\n * `ReadMode` (`null` → `full` backfill, else `delta`), streams `read()`, and\n * stamps each `SourcedRecord` into a `Change<T>`. All records surface as\n * `'updated'`; the orchestrator's diff stage classifies create-vs-update and\n * deletes arrive as tombstone refs (`toCanonical` may flag them).\n *\n * Cursor emission honors `cursorDivisible` (RFC-0003 §3). Divisible: each\n * record carries its own per-ref cursor. Atomic: per-ref cursors are withheld\n * (`undefined`, which the orchestrator skips persisting) and the end-of-walk\n * token rides only on the final record — so a mid-walk crash never persists\n * an unresumable token. If an atomic run yields no surviving records, no\n * cursor is persisted and the next run re-reads the same (empty) delta — a\n * bounded inefficiency, never data loss.\n */\n async *listChanges(\n subscription: IntegrationSubscriptionView,\n cursor: unknown | null,\n ): AsyncIterable<Change<T>> {\n const mode: ReadMode =\n cursor === null || cursor === undefined\n ? { kind: 'full' }\n : { kind: 'delta', cursor };\n const filter = this.filterFor(subscription);\n const stream = this.read({ mode, filter });\n\n if (this.cursorDivisible) {\n for await (const sourced of stream) {\n yield this.toChange(sourced, sourced.cursor);\n }\n return;\n }\n\n // Atomic: one-record lookahead. Emit every record but the last with a\n // withheld (`undefined`) cursor; the last record carries the end-of-walk\n // token. Contract: an atomic adapter stamps the (single, shared) end-of-walk\n // token onto its refs' `cursor` — so whichever record survives last carries\n // it. The base emits a real cursor exactly once, on that final record, so the\n // orchestrator can never persist a mid-walk value. If zero records survive,\n // nothing is persisted (next run re-reads the delta — bounded, never lossy).\n let prev: SourcedRecord<T> | null = null;\n for await (const sourced of stream) {\n if (prev !== null) yield this.toChange(prev, undefined);\n prev = sourced;\n }\n if (prev !== null) yield this.toChange(prev, prev.cursor);\n }\n\n /** Stamp a `SourcedRecord` into a `Change<T>` with an explicit emitted cursor. */\n private toChange(sourced: SourcedRecord<T>, cursor: unknown): Change<T> {\n return {\n externalId: sourced.externalId,\n operation: 'updated',\n record: sourced.record,\n cursor,\n source: this.changeSource,\n };\n }\n}\n"],"mappings":"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","names":[]}
|
|
@@ -6,10 +6,11 @@ export { CompleteRunInput, IIntegrationRunRecorder, IntegrationRunSummary, Recor
|
|
|
6
6
|
export { ILoopbackFingerprintStore } from './integration-loopback.protocol.js';
|
|
7
7
|
export { IEntityChangeSourceRegistry, UnknownEntityError } from './entity-change-source-registry.protocol.js';
|
|
8
8
|
export { MemoryEntityChangeSourceRegistry } from './entity-change-source-registry.memory.js';
|
|
9
|
-
export { CursorStrategy, CursorStrategySchema, DetectionConfig, DetectionConfigSchema, FieldMapping, FieldMappingSchema, PollDetection, PollDetectionSchema, ResolvedFilter, ResolvedFilterSchema, WebhookDetection, WebhookDetectionSchema } from './detection-config.schema.js';
|
|
9
|
+
export { CURSOR_DIVISIBILITY, CursorStrategy, CursorStrategySchema, DetectionConfig, DetectionConfigSchema, FieldMapping, FieldMappingSchema, PollDetection, PollDetectionSchema, ResolvedFilter, ResolvedFilterSchema, WebhookDetection, WebhookDetectionSchema, isDivisibleCursor } from './detection-config.schema.js';
|
|
10
10
|
export { ChangeIterator, ChangeMiddleware, ComposeChangeMiddleware } from './integration-middleware.protocol.js';
|
|
11
11
|
export { createLoopbackMiddleware } from './loopback.middleware.js';
|
|
12
12
|
export { PollChangeSource, PollChangeSourceOptions, PollCursor, PollFetchCallback, PollFetchContext } from './poll-change-source.js';
|
|
13
|
+
export { IncrementalRead, IncrementalReadBase, RandomRead, ReadMode, ReadRequest, Ref, SourcedRecord, mapConcurrent } from './incremental-read.js';
|
|
13
14
|
export { WebhookChangeSource, WebhookChangeSourceOptions, WebhookCursor, WebhookFetchCallback, WebhookFetchContext } from './webhook-change-source.js';
|
|
14
15
|
export { buildChangeSource } from './build-change-source.js';
|
|
15
16
|
export { ENTITY_CHANGE_SOURCE_REGISTRY, INTEGRATION_CHANGE_SOURCE, INTEGRATION_CURSOR_STORE, INTEGRATION_FIELD_DIFFER, INTEGRATION_MODULE_OPTIONS, INTEGRATION_MULTI_TENANT, INTEGRATION_RUN_RECORDER, INTEGRATION_SINK } from './integration.tokens.js';
|
|
@@ -77,12 +77,33 @@ var EventIdCursorSchema = z2.object({
|
|
|
77
77
|
kind: z2.literal("eventId"),
|
|
78
78
|
field: z2.string().min(1)
|
|
79
79
|
});
|
|
80
|
+
var HistoryIdCursorSchema = z2.object({
|
|
81
|
+
kind: z2.literal("historyId"),
|
|
82
|
+
field: z2.string().min(1)
|
|
83
|
+
});
|
|
84
|
+
var SyncTokenCursorSchema = z2.object({
|
|
85
|
+
kind: z2.literal("syncToken"),
|
|
86
|
+
field: z2.string().min(1)
|
|
87
|
+
});
|
|
80
88
|
var CursorStrategySchema = z2.discriminatedUnion("kind", [
|
|
81
89
|
SystemModstampCursorSchema,
|
|
82
90
|
ReplayIdCursorSchema,
|
|
83
91
|
TimestampCursorSchema,
|
|
84
|
-
EventIdCursorSchema
|
|
92
|
+
EventIdCursorSchema,
|
|
93
|
+
HistoryIdCursorSchema,
|
|
94
|
+
SyncTokenCursorSchema
|
|
85
95
|
]);
|
|
96
|
+
var CURSOR_DIVISIBILITY = {
|
|
97
|
+
systemModstamp: true,
|
|
98
|
+
timestamp: true,
|
|
99
|
+
replayId: true,
|
|
100
|
+
eventId: false,
|
|
101
|
+
historyId: false,
|
|
102
|
+
syncToken: false
|
|
103
|
+
};
|
|
104
|
+
function isDivisibleCursor(kind) {
|
|
105
|
+
return CURSOR_DIVISIBILITY[kind];
|
|
106
|
+
}
|
|
86
107
|
var PollDetectionSchema = z2.object({
|
|
87
108
|
cursor: CursorStrategySchema,
|
|
88
109
|
provenance: z2.enum(["poll", "cdc"]).optional()
|
|
@@ -214,6 +235,148 @@ var PollChangeSource = class {
|
|
|
214
235
|
}
|
|
215
236
|
};
|
|
216
237
|
|
|
238
|
+
// runtime/subsystems/integration/incremental-read.ts
|
|
239
|
+
async function mapConcurrent(ids, fn, limit) {
|
|
240
|
+
const out = /* @__PURE__ */ new Map();
|
|
241
|
+
if (ids.length === 0) return out;
|
|
242
|
+
const width = Math.max(1, Math.min(limit, ids.length));
|
|
243
|
+
let next = 0;
|
|
244
|
+
const worker = async () => {
|
|
245
|
+
while (next < ids.length) {
|
|
246
|
+
const idx = next++;
|
|
247
|
+
const id = ids[idx];
|
|
248
|
+
out.set(id, await fn(id));
|
|
249
|
+
}
|
|
250
|
+
};
|
|
251
|
+
await Promise.all(Array.from({ length: width }, worker));
|
|
252
|
+
return out;
|
|
253
|
+
}
|
|
254
|
+
var IncrementalReadBase = class {
|
|
255
|
+
/**
|
|
256
|
+
* Whether the vendor takes the request predicate server-side. Declared, not
|
|
257
|
+
* enforced here — surfaced into the emission manifest (R3) so the falsifier
|
|
258
|
+
* suite (R4) can record which adapters filter post-hydrate. `false` is the
|
|
259
|
+
* honest floor (e.g. Gmail without `q=`), handled via `matchesRecord`.
|
|
260
|
+
*/
|
|
261
|
+
filterPushdown = false;
|
|
262
|
+
/** Max concurrent in-flight calls for a `mapConcurrent`-built `hydrate`. */
|
|
263
|
+
hydrateConcurrency = 10;
|
|
264
|
+
/** `Change<T>.source` provenance stamped by `listChanges`. */
|
|
265
|
+
changeSource = "poll";
|
|
266
|
+
/**
|
|
267
|
+
* Whether this source's cursor strategy is divisible (RFC-0003 §3). When
|
|
268
|
+
* `true` (default — sortable watermarks like `systemModstamp`/`timestamp`/
|
|
269
|
+
* `replayId`), `listChanges` emits each record's per-ref cursor, so the
|
|
270
|
+
* orchestrator may checkpoint mid-walk and a crash resumes from the last
|
|
271
|
+
* delivered ref.
|
|
272
|
+
*
|
|
273
|
+
* When `false` (atomic opaque tokens — Gmail `historyId`, Calendar
|
|
274
|
+
* `syncToken`), `listChanges` WITHHOLDS per-ref cursors and emits the
|
|
275
|
+
* end-of-walk token only on the final record, so the orchestrator's
|
|
276
|
+
* persist-last-yielded lifecycle can never persist an unresumable mid-walk
|
|
277
|
+
* token. The cost is blast-radius: an interrupted atomic run resumes
|
|
278
|
+
* all-or-nothing from the prior persisted token. For atomic *backfills* that
|
|
279
|
+
* radius is the whole enumerate walk — bound it with `ReadRequest.pageSize`
|
|
280
|
+
* (smaller pages ⇒ shorter walks per run). Per-page atomic checkpointing is a
|
|
281
|
+
* future refinement; R2 gates at end-of-walk.
|
|
282
|
+
*
|
|
283
|
+
* Codegen (R3) sets this from the strategy kind via `isDivisibleCursor`.
|
|
284
|
+
*/
|
|
285
|
+
cursorDivisible = true;
|
|
286
|
+
// ---- Optional filter hooks — exactly one is live per `filterPushdown` ----
|
|
287
|
+
/** Pre-hydrate predicate over the cheap ref (preferred — avoids hydration). */
|
|
288
|
+
matchesRef(_ref, _filter) {
|
|
289
|
+
return true;
|
|
290
|
+
}
|
|
291
|
+
/** Post-hydrate predicate over the canonical record (the no-pushdown floor). */
|
|
292
|
+
matchesRecord(_record, _filter) {
|
|
293
|
+
return true;
|
|
294
|
+
}
|
|
295
|
+
/**
|
|
296
|
+
* Resolve the filter for a subscription when adapting to `listChanges`
|
|
297
|
+
* (which has no filter argument). Defaults to none; codegen wiring (R3)
|
|
298
|
+
* overrides this to thread `DetectionConfig.filters`.
|
|
299
|
+
*/
|
|
300
|
+
filterFor(_subscription) {
|
|
301
|
+
return void 0;
|
|
302
|
+
}
|
|
303
|
+
// ---- PROVIDED by the base ----
|
|
304
|
+
/**
|
|
305
|
+
* Stream canonical records for a request. Filter is applied BEFORE hydrate
|
|
306
|
+
* (structural: a kept ref is hydrated, a rejected one never is), so an
|
|
307
|
+
* adapter cannot hydrate-then-discard. A hydrate miss (deleted mid-run) is
|
|
308
|
+
* skipped, never fabricated.
|
|
309
|
+
*/
|
|
310
|
+
async *read(req) {
|
|
311
|
+
for await (const refPage of this.enumerate(req.mode, req.filter, req.pageSize)) {
|
|
312
|
+
const kept = refPage.filter((ref) => this.matchesRef(ref, req.filter));
|
|
313
|
+
if (kept.length === 0) continue;
|
|
314
|
+
const raws = await this.hydrate(kept.map((ref) => ref.externalId));
|
|
315
|
+
for (const ref of kept) {
|
|
316
|
+
const raw = raws.get(ref.externalId);
|
|
317
|
+
if (raw === void 0 || raw === null) continue;
|
|
318
|
+
const record = this.toCanonical(raw);
|
|
319
|
+
if (record !== null && this.matchesRecord(record, req.filter)) {
|
|
320
|
+
yield { externalId: ref.externalId, record, raw, cursor: ref.cursor };
|
|
321
|
+
}
|
|
322
|
+
}
|
|
323
|
+
}
|
|
324
|
+
}
|
|
325
|
+
/**
|
|
326
|
+
* `RandomRead<T>` — single-record read, provided for free as
|
|
327
|
+
* `toCanonical ∘ hydrate([id])`. Reuses the adapter's batched fetch + miss
|
|
328
|
+
* tolerance; returns `null` for a missing or undecodable record.
|
|
329
|
+
*/
|
|
330
|
+
async get(id) {
|
|
331
|
+
const raws = await this.hydrate([id]);
|
|
332
|
+
const raw = raws.get(id);
|
|
333
|
+
if (raw === void 0 || raw === null) return null;
|
|
334
|
+
return this.toCanonical(raw);
|
|
335
|
+
}
|
|
336
|
+
/**
|
|
337
|
+
* `IChangeSource<T>` adaptation. Maps the orchestrator's by-value cursor to a
|
|
338
|
+
* `ReadMode` (`null` → `full` backfill, else `delta`), streams `read()`, and
|
|
339
|
+
* stamps each `SourcedRecord` into a `Change<T>`. All records surface as
|
|
340
|
+
* `'updated'`; the orchestrator's diff stage classifies create-vs-update and
|
|
341
|
+
* deletes arrive as tombstone refs (`toCanonical` may flag them).
|
|
342
|
+
*
|
|
343
|
+
* Cursor emission honors `cursorDivisible` (RFC-0003 §3). Divisible: each
|
|
344
|
+
* record carries its own per-ref cursor. Atomic: per-ref cursors are withheld
|
|
345
|
+
* (`undefined`, which the orchestrator skips persisting) and the end-of-walk
|
|
346
|
+
* token rides only on the final record — so a mid-walk crash never persists
|
|
347
|
+
* an unresumable token. If an atomic run yields no surviving records, no
|
|
348
|
+
* cursor is persisted and the next run re-reads the same (empty) delta — a
|
|
349
|
+
* bounded inefficiency, never data loss.
|
|
350
|
+
*/
|
|
351
|
+
async *listChanges(subscription, cursor) {
|
|
352
|
+
const mode = cursor === null || cursor === void 0 ? { kind: "full" } : { kind: "delta", cursor };
|
|
353
|
+
const filter = this.filterFor(subscription);
|
|
354
|
+
const stream = this.read({ mode, filter });
|
|
355
|
+
if (this.cursorDivisible) {
|
|
356
|
+
for await (const sourced of stream) {
|
|
357
|
+
yield this.toChange(sourced, sourced.cursor);
|
|
358
|
+
}
|
|
359
|
+
return;
|
|
360
|
+
}
|
|
361
|
+
let prev = null;
|
|
362
|
+
for await (const sourced of stream) {
|
|
363
|
+
if (prev !== null) yield this.toChange(prev, void 0);
|
|
364
|
+
prev = sourced;
|
|
365
|
+
}
|
|
366
|
+
if (prev !== null) yield this.toChange(prev, prev.cursor);
|
|
367
|
+
}
|
|
368
|
+
/** Stamp a `SourcedRecord` into a `Change<T>` with an explicit emitted cursor. */
|
|
369
|
+
toChange(sourced, cursor) {
|
|
370
|
+
return {
|
|
371
|
+
externalId: sourced.externalId,
|
|
372
|
+
operation: "updated",
|
|
373
|
+
record: sourced.record,
|
|
374
|
+
cursor,
|
|
375
|
+
source: this.changeSource
|
|
376
|
+
};
|
|
377
|
+
}
|
|
378
|
+
};
|
|
379
|
+
|
|
217
380
|
// runtime/subsystems/integration/webhook-change-source.ts
|
|
218
381
|
var WebhookChangeSource = class {
|
|
219
382
|
label;
|
|
@@ -1186,6 +1349,7 @@ IntegrationModule = __decorateClass([
|
|
|
1186
1349
|
Module({})
|
|
1187
1350
|
], IntegrationModule);
|
|
1188
1351
|
export {
|
|
1352
|
+
CURSOR_DIVISIBILITY,
|
|
1189
1353
|
CursorStrategySchema,
|
|
1190
1354
|
DeepEqualDiffer,
|
|
1191
1355
|
DetectionConfigSchema,
|
|
@@ -1202,6 +1366,7 @@ export {
|
|
|
1202
1366
|
INTEGRATION_MULTI_TENANT,
|
|
1203
1367
|
INTEGRATION_RUN_RECORDER,
|
|
1204
1368
|
INTEGRATION_SINK,
|
|
1369
|
+
IncrementalReadBase,
|
|
1205
1370
|
IntegrationModule,
|
|
1206
1371
|
MemoryCursorStore,
|
|
1207
1372
|
MemoryEntityChangeSourceRegistry,
|
|
@@ -1224,6 +1389,8 @@ export {
|
|
|
1224
1389
|
integrationRunItems,
|
|
1225
1390
|
integrationRunStatusEnum,
|
|
1226
1391
|
integrationRuns,
|
|
1227
|
-
integrationSubscriptions
|
|
1392
|
+
integrationSubscriptions,
|
|
1393
|
+
isDivisibleCursor,
|
|
1394
|
+
mapConcurrent
|
|
1228
1395
|
};
|
|
1229
1396
|
//# sourceMappingURL=index.js.map
|