@voltro/database 0.55.0 → 0.57.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 +691 -0
- package/dist/index.d.ts +174 -3
- package/dist/index.js +387 -318
- package/dist/sql.d.ts +15 -24
- package/dist/sql.js +200 -180
- package/package.json +2 -2
package/dist/index.d.ts
CHANGED
|
@@ -621,6 +621,11 @@ export declare interface CdcConfig {
|
|
|
621
621
|
readonly exclusionRefreshFollows?: boolean;
|
|
622
622
|
}
|
|
623
623
|
|
|
624
|
+
/** Env var that tunes how long a change-claim row survives. Declared here rather
|
|
625
|
+
* than at the retention registration, because the OCCURRENCE COUNTER's window
|
|
626
|
+
* has to be derived from the same number and lives in this file. */
|
|
627
|
+
export declare const CHANGE_CLAIM_TTL_ENV = "VOLTRO_CHANGE_CLAIMS_TTL_HOURS";
|
|
628
|
+
|
|
624
629
|
/** Maximum `onChange` listeners a DataStore's change bus accepts before Node
|
|
625
630
|
* warns. See this file's header for why it is 512 and why it is not `0`. */
|
|
626
631
|
export declare const CHANGE_LISTENER_CEILING = 512;
|
|
@@ -638,6 +643,17 @@ export declare interface ChangeCaptureExclusionRefresh {
|
|
|
638
643
|
/** A row was removed from the table. `new` is null, always. */
|
|
639
644
|
export declare const changeDelete: (table: string, row: Row, meta?: ChangeEventMeta) => ChangeEvent;
|
|
640
645
|
|
|
646
|
+
/** The content half of a change identity — equal iff two replicas are looking
|
|
647
|
+
* at the same change (or at two changes with byte-identical images, which the
|
|
648
|
+
* occurrence counter then separates). */
|
|
649
|
+
export declare const changeDigest: (input: {
|
|
650
|
+
readonly pipe: string;
|
|
651
|
+
readonly op: "insert" | "update" | "delete";
|
|
652
|
+
readonly key: string;
|
|
653
|
+
readonly next: Record<string, unknown> | null;
|
|
654
|
+
readonly prev: Record<string, unknown> | null;
|
|
655
|
+
}) => string;
|
|
656
|
+
|
|
641
657
|
/**
|
|
642
658
|
* A row-change event emitted by the data store (in-memory) or the CDC
|
|
643
659
|
* consumer (Postgres logical replication). Drives the reactive engine.
|
|
@@ -766,6 +782,22 @@ export declare const changeSoftDelete: (table: string, row: Row, opts?: {
|
|
|
766
782
|
*/
|
|
767
783
|
export declare type ChangeStrategy = 'inline' | 'cdc';
|
|
768
784
|
|
|
785
|
+
/**
|
|
786
|
+
* A proven interruption in a store's change stream.
|
|
787
|
+
*
|
|
788
|
+
* Deliberately NOT a list of what was missed. A pub/sub transport keeps no log,
|
|
789
|
+
* so the missed changes are gone — the only honest recovery is to re-derive
|
|
790
|
+
* everything downstream, and a consumer that were handed a partial list would be
|
|
791
|
+
* tempted to patch instead. `reason` is for the log line; the instruction is the
|
|
792
|
+
* event itself.
|
|
793
|
+
*/
|
|
794
|
+
export declare interface ChangeStreamGap {
|
|
795
|
+
/** Human-readable cause — goes into the refresh's log line. */
|
|
796
|
+
readonly reason: string;
|
|
797
|
+
/** Wall-clock ms the stream was known to be down, when it is known. */
|
|
798
|
+
readonly downForMs?: number;
|
|
799
|
+
}
|
|
800
|
+
|
|
769
801
|
/** A row changed. Both sides are present, because an update has both. */
|
|
770
802
|
export declare const changeUpdate: (table: string, before: Row, after: Row, meta?: ChangeEventMeta) => ChangeEvent;
|
|
771
803
|
|
|
@@ -1772,6 +1804,9 @@ export declare const compileRawFragment: <Row extends object = Record<string, un
|
|
|
1772
1804
|
|
|
1773
1805
|
export declare const compileSelect: <Row extends object>(descriptor: QueryDescriptor, sql: SqlConstructor, namespace?: QueryNamespace) => Statement.Statement<Row>;
|
|
1774
1806
|
|
|
1807
|
+
/** Compose the durable identity written to `_voltro_cdcout_claims.changeKey`. */
|
|
1808
|
+
export declare const composeChangeKey: (digest: string, occurrence: number) => string;
|
|
1809
|
+
|
|
1775
1810
|
/**
|
|
1776
1811
|
* Compute the embedded row from a {@link VectorEmbeddingContext}. Returns
|
|
1777
1812
|
* the patch to merge into the row: `{ [as]: <vector> }`. When the source
|
|
@@ -2298,6 +2333,23 @@ export declare interface DataStore {
|
|
|
2298
2333
|
* must elect a single worker instead of filtering by origin.
|
|
2299
2334
|
*/
|
|
2300
2335
|
readonly changeScope?: 'local' | 'fleet';
|
|
2336
|
+
/**
|
|
2337
|
+
* Subscribe to CHANGE-STREAM GAPS — moments where this process proved it
|
|
2338
|
+
* stopped receiving changes for a while and has now started again.
|
|
2339
|
+
*
|
|
2340
|
+
* A gap is not an error to show a user; it is an instruction to the reactive
|
|
2341
|
+
* layer: everything derived from this store may be stale, so re-run it. A
|
|
2342
|
+
* live query is idempotent, which is what makes "re-run all of them" both
|
|
2343
|
+
* safe and the complete recovery. `voltro dev` and `voltro serve` wire this
|
|
2344
|
+
* to the dispatcher's `refreshAll`, the same recovery the broadcast bus's
|
|
2345
|
+
* peer-serial gap uses.
|
|
2346
|
+
*
|
|
2347
|
+
* Optional because only a store whose changes arrive over a TRANSPORT can
|
|
2348
|
+
* have one. An inline store emits its own writes in-process — there is no
|
|
2349
|
+
* connection to lose — and correctly does not implement this. Absent
|
|
2350
|
+
* therefore means "gaps are impossible here", never "gaps are not reported".
|
|
2351
|
+
*/
|
|
2352
|
+
readonly onChangeStreamGap?: (listener: (gap: ChangeStreamGap) => void) => () => void;
|
|
2301
2353
|
/**
|
|
2302
2354
|
* Bind a per-request tenant NAMESPACE, returning a store view whose
|
|
2303
2355
|
* every read + write is physically isolated to that namespace
|
|
@@ -2724,6 +2776,10 @@ export declare const decryptFieldsOnRead: (rows: ReadonlyArray<Row>, table: Tabl
|
|
|
2724
2776
|
*/
|
|
2725
2777
|
export declare const DEFAULT_ACQUIRE_TIMEOUT_MS = 10000;
|
|
2726
2778
|
|
|
2779
|
+
/** Default lifetime of a change-claim row, in hours. A lock ledger, not a
|
|
2780
|
+
* history — nobody reads yesterday's claim. */
|
|
2781
|
+
export declare const DEFAULT_CHANGE_CLAIM_TTL_HOURS = 1;
|
|
2782
|
+
|
|
2727
2783
|
/**
|
|
2728
2784
|
* How often a persistent fallback is allowed to say so.
|
|
2729
2785
|
*
|
|
@@ -3498,7 +3554,6 @@ export declare const formatSubjectGraph: (graph: SubjectGraph) => string;
|
|
|
3498
3554
|
*/
|
|
3499
3555
|
export declare const FRAMEWORK_LIVE_TABLES: ReadonlySet<string>;
|
|
3500
3556
|
|
|
3501
|
-
/** All framework tables as an array — useful for bootstrap DDL emission. */
|
|
3502
3557
|
export declare const frameworkTables: readonly [TableLike, TableLike, TableLike];
|
|
3503
3558
|
|
|
3504
3559
|
declare type FullTextIndexMeta = {
|
|
@@ -4601,6 +4656,67 @@ export declare const numeric: (precision: number, scale?: number) => ColumnBuild
|
|
|
4601
4656
|
*/
|
|
4602
4657
|
export declare const observeDbOp: <T>(dialect: DialectId | string, op: DbOp, run: () => Promise<T>) => Promise<T>;
|
|
4603
4658
|
|
|
4659
|
+
/**
|
|
4660
|
+
* How many times this replica has already seen each digest inside the dedup
|
|
4661
|
+
* window. Bounded on BOTH axes — a per-change map with no ceiling is an
|
|
4662
|
+
* unbounded table in memory:
|
|
4663
|
+
* * by time — entries untouched for `windowMs` are pruned, and
|
|
4664
|
+
* * by size — `maxEntries` oldest-first, so a burst of unique changes can't
|
|
4665
|
+
* grow it without limit.
|
|
4666
|
+
*
|
|
4667
|
+
* `windowMs` MUST be longer than the lifetime of a claim row in the database.
|
|
4668
|
+
* That ordering is the invariant: if a claim outlived the counter that
|
|
4669
|
+
* produced it, the next sighting of that digest would restart at occurrence 0,
|
|
4670
|
+
* collide with the retained claim, and be dropped as a duplicate it is not.
|
|
4671
|
+
*
|
|
4672
|
+
* **`maxEntries` is the SAME invariant reached by size, and it is worth saying
|
|
4673
|
+
* plainly rather than reassuringly.** An evicted digest also restarts at 0, so
|
|
4674
|
+
* an eviction is only harmless while no claim for that digest is still retained
|
|
4675
|
+
* — which is not the same thing as "the position a freshly-booted replica is
|
|
4676
|
+
* in", because a fresh replica has no claims of its own to collide with. The
|
|
4677
|
+
* exposure is narrow (it needs more than `maxEntries` distinct digests inside
|
|
4678
|
+
* the claim TTL *and* a content-identical repeat of an evicted one), and it is
|
|
4679
|
+
* real. Size the map against the change rate the gated tables actually carry,
|
|
4680
|
+
* the way `plugin-cdc-out` sizes it against its handoff buffer.
|
|
4681
|
+
*/
|
|
4682
|
+
export declare class OccurrenceCounter {
|
|
4683
|
+
#private;
|
|
4684
|
+
constructor(opts: {
|
|
4685
|
+
readonly windowMs: number;
|
|
4686
|
+
readonly maxEntries: number;
|
|
4687
|
+
});
|
|
4688
|
+
get size(): number;
|
|
4689
|
+
/** Record a sighting and return its occurrence index (0 for the first). */
|
|
4690
|
+
next(digest: string, nowMs: number): number;
|
|
4691
|
+
/**
|
|
4692
|
+
* Align this replica with the fleet from the claims already recorded in the
|
|
4693
|
+
* database — the counters of a process that booted mid-window would
|
|
4694
|
+
* otherwise restart at 0 and re-key changes the incumbents have already
|
|
4695
|
+
* keyed. Call ONCE, before the first observed change is keyed.
|
|
4696
|
+
*/
|
|
4697
|
+
seedFromChangeKeys(changeKeys: Iterable<string>, nowMs: number): void;
|
|
4698
|
+
/** Drop entries whose last sighting is older than the window. */
|
|
4699
|
+
prune(nowMs: number): void;
|
|
4700
|
+
}
|
|
4701
|
+
|
|
4702
|
+
/**
|
|
4703
|
+
* The counter window that keeps {@link OccurrenceCounter}'s invariant true for
|
|
4704
|
+
* the claim TTL this deployment actually runs with.
|
|
4705
|
+
*
|
|
4706
|
+
* The invariant is stated on the class: the window MUST be longer than the
|
|
4707
|
+
* lifetime of a claim row, or a digest whose counter entry was pruned restarts
|
|
4708
|
+
* at occurrence 0, collides with the retained claim, and is dropped as a
|
|
4709
|
+
* duplicate it is not. Three call sites hard-coded two hours against a
|
|
4710
|
+
* one-hour default — correct until an operator raises the TTL, at which point
|
|
4711
|
+
* the invariant breaks SILENTLY and in the direction that loses effects.
|
|
4712
|
+
*
|
|
4713
|
+
* So it is derived, not written down: twice the TTL, from the same env var the
|
|
4714
|
+
* retention policy reads.
|
|
4715
|
+
*/
|
|
4716
|
+
export declare const occurrenceWindowMs: (env?: {
|
|
4717
|
+
readonly [key: string]: string | undefined;
|
|
4718
|
+
}) => number;
|
|
4719
|
+
|
|
4604
4720
|
/**
|
|
4605
4721
|
* What a decrypt failure does. `'throw'` (default, and the ONLY safe production
|
|
4606
4722
|
* behaviour) surfaces a typed `FieldDecryptionError`. `'null'` degrades the one
|
|
@@ -4732,6 +4848,12 @@ export declare const paginateBy: (descriptor: QueryDescriptor, column: string, c
|
|
|
4732
4848
|
*/
|
|
4733
4849
|
export declare const paginateById: (descriptor: QueryDescriptor, cursor: string | undefined, limit: number) => QueryDescriptor;
|
|
4734
4850
|
|
|
4851
|
+
/** Split a stored `changeKey` back into its two halves (`null` when malformed). */
|
|
4852
|
+
export declare const parseChangeKey: (changeKey: string) => {
|
|
4853
|
+
digest: string;
|
|
4854
|
+
occurrence: number;
|
|
4855
|
+
} | null;
|
|
4856
|
+
|
|
4735
4857
|
/** Test seam — how many registrations are outstanding. */
|
|
4736
4858
|
export declare const pendingAttributionCount: () => number;
|
|
4737
4859
|
|
|
@@ -8050,8 +8172,20 @@ export declare const _voltroDataTransfersTable: TableLike;
|
|
|
8050
8172
|
* (the `Idempotency-Key` header on REST routes). `scope` namespaces the key
|
|
8051
8173
|
* per tenant + method + path; the UNIQUE on (scope, key) is the atomic claim
|
|
8052
8174
|
* arbiter (INSERT/upsert-on-conflict). `response` holds the cached
|
|
8053
|
-
* `{ status, body }` for replay.
|
|
8054
|
-
*
|
|
8175
|
+
* `{ status, body }` for replay.
|
|
8176
|
+
*
|
|
8177
|
+
* Created for EVERY sql app (`when: 'always'` in the cli's registry), empty
|
|
8178
|
+
* unless `idempotency` is set in `app.config.ts`. This used to say "created only
|
|
8179
|
+
* when idempotency is set", which is what the config gates — not what the table
|
|
8180
|
+
* registry does.
|
|
8181
|
+
*
|
|
8182
|
+
* Two things clean it up, and only one of them existed for most of this table's
|
|
8183
|
+
* life. The LAZY TTL drops a stale row when the SAME key is claimed again — and
|
|
8184
|
+
* a key is used once by definition, so a row whose key never returns was never
|
|
8185
|
+
* read and never deleted. The retention SWEEP is the half that bounds it
|
|
8186
|
+
* (`retentionSweep.ts`), and its window is floored by the app's own dedup window:
|
|
8187
|
+
* a record dropped while still inside that window lets the duplicate request it
|
|
8188
|
+
* exists to stop execute a second time.
|
|
8055
8189
|
*/
|
|
8056
8190
|
export declare const _voltroIdempotencyTable: TableLike;
|
|
8057
8191
|
|
|
@@ -8104,6 +8238,43 @@ export declare const _voltroMssqlCdcOffsetsTable: TableLike;
|
|
|
8104
8238
|
*/
|
|
8105
8239
|
export declare const _voltroReplaceInProgressTable: TableLike;
|
|
8106
8240
|
|
|
8241
|
+
/**
|
|
8242
|
+
* `_voltro_replica_observations` — what each replica knows about itself, put
|
|
8243
|
+
* somewhere every replica can read it.
|
|
8244
|
+
*
|
|
8245
|
+
* ── Why the shared store and not a fan-out ────────────────────────────────
|
|
8246
|
+
*
|
|
8247
|
+
* The fleet-wide questions an operator asks — "how many subscriptions is each
|
|
8248
|
+
* pod holding", "which of my queries can resume anywhere" — are answers each
|
|
8249
|
+
* process holds privately. Asking the others at read time means inventing
|
|
8250
|
+
* request/response over a pub/sub bus: a correlation id, a reply channel, a
|
|
8251
|
+
* deadline to guess, and a partial-result protocol. And it puts the DIAGNOSTIC
|
|
8252
|
+
* on the failure path, so the moment you most need it is the moment it times
|
|
8253
|
+
* out.
|
|
8254
|
+
*
|
|
8255
|
+
* Writing instead inverts that. The transport is the database — the one thing
|
|
8256
|
+
* that must be up for anything to work — and a replica that dies goes STALE
|
|
8257
|
+
* rather than silent: staleness is a number, and a number can be reported.
|
|
8258
|
+
* Paired with `instanceMembership`'s roster, "old" becomes "missing", which is
|
|
8259
|
+
* the honest partial result the fan-out would have had to build by hand.
|
|
8260
|
+
*
|
|
8261
|
+
* `coordinationState.recentReplicaIds` already answers a fleet question this
|
|
8262
|
+
* way, by reading `_voltro_schedule_runs`. This generalises that one case.
|
|
8263
|
+
*
|
|
8264
|
+
* ── The shape, and what it must not become ────────────────────────────────
|
|
8265
|
+
*
|
|
8266
|
+
* One row per `(replicaId, kind)`, upserted. So the table is bounded by fleet
|
|
8267
|
+
* size times the number of kinds, not by time — there is no history here and
|
|
8268
|
+
* there must not be: a history would need a retention policy, and this is a
|
|
8269
|
+
* cache of the present, not a log of the past.
|
|
8270
|
+
*
|
|
8271
|
+
* `payload` is a JSON STRING and small on purpose. Anything big enough to
|
|
8272
|
+
* warrant paging belongs on the process that holds it, reachable per replica —
|
|
8273
|
+
* writing a full subscription list here every heartbeat would be write
|
|
8274
|
+
* amplification for state nobody is reading.
|
|
8275
|
+
*/
|
|
8276
|
+
export declare const _voltroReplicaObservationsTable: TableLike;
|
|
8277
|
+
|
|
8107
8278
|
/**
|
|
8108
8279
|
* `_voltro_seeds` — one row per registered seed. Written to by the
|
|
8109
8280
|
* seed runner as each seed's lifecycle changes. Unlike migrations,
|