@voltro/plugin-cdc-out 0.32.0 → 0.34.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 +2006 -0
- package/dist/index.d.ts +153 -0
- package/dist/index.js +376 -146
- package/package.json +5 -4
package/dist/index.d.ts
CHANGED
|
@@ -6,6 +6,14 @@ import { Schema } from 'effect';
|
|
|
6
6
|
import { TableLike } from '@voltro/database';
|
|
7
7
|
import { VoltroPlugin } from '@voltro/protocol';
|
|
8
8
|
|
|
9
|
+
/**
|
|
10
|
+
* Deterministic JSON — object keys sorted at every depth, so two replicas
|
|
11
|
+
* that decoded the same row into differently-ordered objects still digest
|
|
12
|
+
* identically. `Date`s normalise to ISO (a binlog reader hands back `Date`s
|
|
13
|
+
* where a NOTIFY payload hands back strings), `undefined` to null.
|
|
14
|
+
*/
|
|
15
|
+
export declare const canonicalJson: (value: unknown) => string;
|
|
16
|
+
|
|
9
17
|
/**
|
|
10
18
|
* Thrown at plugin-construction time (= `app.config.ts` load = boot) when the
|
|
11
19
|
* cdc-out config is malformed: no sinks, a sink without `deliver`, a duplicate
|
|
@@ -73,10 +81,25 @@ export declare class CdcLeaseManager {
|
|
|
73
81
|
release(store: DataStore): Promise<void>;
|
|
74
82
|
}
|
|
75
83
|
|
|
84
|
+
export declare const CDCOUT_CLAIMS_TABLE = "_voltro_cdcout_claims";
|
|
85
|
+
|
|
76
86
|
export declare const CDCOUT_LEASES_TABLE = "_voltro_cdcout_leases";
|
|
77
87
|
|
|
78
88
|
export declare const CDCOUT_OUTBOX_TABLE = "_voltro_cdcout_outbox";
|
|
79
89
|
|
|
90
|
+
/**
|
|
91
|
+
* The enqueue idempotence key. `unique(pipe, changeKey)` is the whole point:
|
|
92
|
+
* an enqueue is an `insertIgnore` here FIRST, and only the replica whose
|
|
93
|
+
* `claimedBy` comes back writes the outbox row. That is what lets every
|
|
94
|
+
* replica try during a leadership gap without producing duplicates.
|
|
95
|
+
*
|
|
96
|
+
* `claimedBy` also makes the claim RE-ENTRANT for its owner: a replica that
|
|
97
|
+
* claimed and then failed the outbox insert reads its own id back on the next
|
|
98
|
+
* drain and finishes the job, instead of treating its own claim as somebody
|
|
99
|
+
* else's and dropping the change.
|
|
100
|
+
*/
|
|
101
|
+
export declare const cdcOutClaimsTable: TableLike;
|
|
102
|
+
|
|
80
103
|
export declare class CdcOutEngine {
|
|
81
104
|
private readonly opts;
|
|
82
105
|
private readonly now;
|
|
@@ -119,6 +142,24 @@ export declare interface CdcOutOptions {
|
|
|
119
142
|
* (`@voltro/plugin-cdc-out#analytics`), the inspect mount
|
|
120
143
|
* (`…/plugins/cdc-out--analytics/…`) and the outbox pipe keys.
|
|
121
144
|
*/
|
|
145
|
+
/**
|
|
146
|
+
* Namespace for this plugin's inspect endpoints. Default `cdc-out`.
|
|
147
|
+
*
|
|
148
|
+
* Set it when your app already publishes under that name — an exact tag
|
|
149
|
+
* collision is fatal at codegen, and this is the way out. Orthogonal to
|
|
150
|
+
* `name` below: `alias` REPLACES the namespace, `name` distinguishes two
|
|
151
|
+
* installations within it.
|
|
152
|
+
*
|
|
153
|
+
* The cost, stated because nothing else states it: the local and cloud
|
|
154
|
+
* dashboards fetch this plugin's panel at the DEFAULT slug, so an aliased
|
|
155
|
+
* install keeps working while its dashboard panel 404s. Alias to escape a
|
|
156
|
+
* collision, not for taste.
|
|
157
|
+
*/
|
|
158
|
+
readonly alias?: string;
|
|
159
|
+
/**
|
|
160
|
+
* Discriminator for a SECOND installation of this plugin, when one app runs
|
|
161
|
+
* two (`@voltro/plugin-cdc-out#analytics`). Not a rename — for that use `alias`.
|
|
162
|
+
*/
|
|
122
163
|
readonly name?: string;
|
|
123
164
|
/** First retry delay; doubles per attempt with jitter (default 200). */
|
|
124
165
|
readonly backoffBaseMs?: number;
|
|
@@ -133,6 +174,27 @@ export declare interface CdcOutOptions {
|
|
|
133
174
|
/** Retention for delivered/dead outbox rows, in hours (default: the
|
|
134
175
|
* `CDCOUT_RETENTION_HOURS` env var, else 72). */
|
|
135
176
|
readonly retentionHours?: number;
|
|
177
|
+
/**
|
|
178
|
+
* How long an enqueue claim (`_voltro_cdcout_claims`) is kept — the window
|
|
179
|
+
* inside which two replicas racing to enqueue the same fleet change
|
|
180
|
+
* collapse to one row. It only has to outlive a leadership handoff, so the
|
|
181
|
+
* default is `max(60_000, 4 × leaseTtlMs)`.
|
|
182
|
+
*
|
|
183
|
+
* Raising it costs one small row per change for longer and widens the span
|
|
184
|
+
* over which two byte-identical changes to the same row are told apart by
|
|
185
|
+
* their occurrence counter rather than by content. Lowering it below
|
|
186
|
+
* `leaseTtlMs + one heartbeat` is what re-opens the duplicate window.
|
|
187
|
+
*/
|
|
188
|
+
readonly dedupWindowMs?: number;
|
|
189
|
+
/** How far back each replica keeps observed fleet changes in memory for a
|
|
190
|
+
* takeover to drain (default: `dedupWindowMs`). A handoff that takes longer
|
|
191
|
+
* than this loses the changes older than the window — reported as
|
|
192
|
+
* `droppedPending` on the `/sinks` inspect endpoint. */
|
|
193
|
+
readonly handoffBufferMs?: number;
|
|
194
|
+
/** Hard ceiling on buffered fleet changes per instance — oldest dropped
|
|
195
|
+
* first (default 10_000). Bounds the memory a write burst during a
|
|
196
|
+
* leadership gap can cost. */
|
|
197
|
+
readonly handoffBufferSize?: number;
|
|
136
198
|
}
|
|
137
199
|
|
|
138
200
|
export declare const cdcOutOutboxTable: TableLike;
|
|
@@ -152,6 +214,20 @@ export declare interface CdcSink {
|
|
|
152
214
|
readonly outboundHost?: string;
|
|
153
215
|
}
|
|
154
216
|
|
|
217
|
+
/** The content half of a change identity — equal iff two replicas are looking
|
|
218
|
+
* at the same change (or at two changes with byte-identical images, which the
|
|
219
|
+
* occurrence counter then separates). */
|
|
220
|
+
export declare const changeDigest: (input: {
|
|
221
|
+
readonly pipe: string;
|
|
222
|
+
readonly op: "insert" | "update" | "delete";
|
|
223
|
+
readonly key: string;
|
|
224
|
+
readonly next: Record<string, unknown> | null;
|
|
225
|
+
readonly prev: Record<string, unknown> | null;
|
|
226
|
+
}) => string;
|
|
227
|
+
|
|
228
|
+
/** Compose the durable identity written to `_voltro_cdcout_claims.changeKey`. */
|
|
229
|
+
export declare const composeChangeKey: (digest: string, occurrence: number) => string;
|
|
230
|
+
|
|
155
231
|
/** Passed to `deliver` — abort fires when the attempt times out
|
|
156
232
|
* (`deliveryTimeoutMs`) so an HTTP sink can cancel its request. */
|
|
157
233
|
export declare interface DeliverContext {
|
|
@@ -192,6 +268,41 @@ export declare const enqueueBackfill: (store: DataStore, config: SinkConfig, row
|
|
|
192
268
|
readonly now?: () => number;
|
|
193
269
|
}) => Promise<number>;
|
|
194
270
|
|
|
271
|
+
export declare class HandoffBuffer<T> {
|
|
272
|
+
#private;
|
|
273
|
+
constructor(opts: {
|
|
274
|
+
readonly windowMs: number;
|
|
275
|
+
readonly maxEntries: number;
|
|
276
|
+
});
|
|
277
|
+
get size(): number;
|
|
278
|
+
/** Entries evicted (by age or ceiling) while still un-enqueued — the honest
|
|
279
|
+
* count of changes the buffer could NOT carry across a handoff. */
|
|
280
|
+
get droppedPending(): number;
|
|
281
|
+
append(entry: HandoffEntry<T>): void;
|
|
282
|
+
/** The oldest entry still awaiting enqueue, or undefined. Drained in
|
|
283
|
+
* observation order so the outbox ids stay commit-ordered. */
|
|
284
|
+
nextPending(): HandoffEntry<T> | undefined;
|
|
285
|
+
pendingCount(): number;
|
|
286
|
+
/** Forget entries older than the window (already-enqueued ones silently,
|
|
287
|
+
* un-enqueued ones counted). */
|
|
288
|
+
prune(nowMs: number): void;
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
/** One observed change, waiting to be enqueued (or already enqueued). */
|
|
292
|
+
export declare interface HandoffEntry<T> {
|
|
293
|
+
/** Pipe key — `<instance>:<table>:<sink name>`. */
|
|
294
|
+
readonly pipe: string;
|
|
295
|
+
/** The durable change identity this entry will be claimed under. Computed
|
|
296
|
+
* at OBSERVATION time, so re-draining an entry re-uses the same key and
|
|
297
|
+
* collapses instead of duplicating. */
|
|
298
|
+
readonly changeKey: string;
|
|
299
|
+
readonly change: T;
|
|
300
|
+
readonly observedAtMs: number;
|
|
301
|
+
/** Set once the entry has been resolved — enqueued by us, or found already
|
|
302
|
+
* claimed by another replica. */
|
|
303
|
+
enqueued: boolean;
|
|
304
|
+
}
|
|
305
|
+
|
|
195
306
|
/** Map one ChangeEvent through a config's filter + map, or null when
|
|
196
307
|
* filtered / unkeyable. */
|
|
197
308
|
export declare const mapChange: (event: PluginChangeEvent, config: SinkConfig) => MappedChange | null;
|
|
@@ -210,10 +321,46 @@ export declare const memorySink: (name?: string) => CdcSink & {
|
|
|
210
321
|
readonly delivered: ReadonlyArray<SinkRecord>;
|
|
211
322
|
};
|
|
212
323
|
|
|
324
|
+
/** Generate the next enqueue-claim id. */
|
|
325
|
+
export declare const nextClaimId: () => string;
|
|
326
|
+
|
|
213
327
|
/** Generate the next outbox id (== deliveryKey). Called SYNCHRONOUSLY in the
|
|
214
328
|
* change tap, so ids carry commit order (uuidv7 is monotonic per process). */
|
|
215
329
|
export declare const nextOutboxId: () => string;
|
|
216
330
|
|
|
331
|
+
/**
|
|
332
|
+
* How many times this replica has already seen each digest inside the dedup
|
|
333
|
+
* window. Bounded on BOTH axes — a per-change map with no ceiling is an
|
|
334
|
+
* unbounded table in memory:
|
|
335
|
+
* * by time — entries untouched for `windowMs` are pruned, and
|
|
336
|
+
* * by size — `maxEntries` oldest-first, so a burst of unique changes can't
|
|
337
|
+
* grow it without limit.
|
|
338
|
+
*
|
|
339
|
+
* `windowMs` MUST be longer than the lifetime of a claim row in the database.
|
|
340
|
+
* That ordering is the invariant: if a claim outlived the counter that
|
|
341
|
+
* produced it, the next sighting of that digest would restart at occurrence 0,
|
|
342
|
+
* collide with the retained claim, and be dropped as a duplicate it is not.
|
|
343
|
+
*/
|
|
344
|
+
export declare class OccurrenceCounter {
|
|
345
|
+
#private;
|
|
346
|
+
constructor(opts: {
|
|
347
|
+
readonly windowMs: number;
|
|
348
|
+
readonly maxEntries: number;
|
|
349
|
+
});
|
|
350
|
+
get size(): number;
|
|
351
|
+
/** Record a sighting and return its occurrence index (0 for the first). */
|
|
352
|
+
next(digest: string, nowMs: number): number;
|
|
353
|
+
/**
|
|
354
|
+
* Align this replica with the fleet from the claims already recorded in the
|
|
355
|
+
* database — the counters of a process that booted mid-window would
|
|
356
|
+
* otherwise restart at 0 and re-key changes the incumbents have already
|
|
357
|
+
* keyed. Call ONCE, before the first observed change is keyed.
|
|
358
|
+
*/
|
|
359
|
+
seedFromChangeKeys(changeKeys: Iterable<string>, nowMs: number): void;
|
|
360
|
+
/** Drop entries whose last sighting is older than the window. */
|
|
361
|
+
prune(nowMs: number): void;
|
|
362
|
+
}
|
|
363
|
+
|
|
217
364
|
export declare interface OutboxRow {
|
|
218
365
|
readonly id: string;
|
|
219
366
|
readonly pipe: string;
|
|
@@ -237,6 +384,12 @@ export declare const outboxRowFor: (mapped: MappedChange, pipe: string, nowMs: n
|
|
|
237
384
|
|
|
238
385
|
export declare type OutboxStatus = 'pending' | 'delivering' | 'delivered' | 'dead';
|
|
239
386
|
|
|
387
|
+
/** Split a stored `changeKey` back into its two halves (`null` when malformed). */
|
|
388
|
+
export declare const parseChangeKey: (changeKey: string) => {
|
|
389
|
+
digest: string;
|
|
390
|
+
occurrence: number;
|
|
391
|
+
} | null;
|
|
392
|
+
|
|
240
393
|
/** Pipe identity — `<instance>:<table>:<sink name>` (the outbox `pipe` column). */
|
|
241
394
|
export declare const pipeKey: (instanceName: string, config: SinkConfig) => string;
|
|
242
395
|
|