/** * The copy reconcile (journal spec §1.5 N7, H3; T13335). * * Every rebind the open pass makes (a copy, a move, a rollback, another * device's store) discards the inherited pull cursor, pauses push and records * `sync.reconcile_due` (`replica.ts`). The new replica then reconciles its * own state against the stream's MERGED state: the latest verified * checkpoint restored into a scratch store (`sync: 'off'`) and pulled to head * there with the merge engine (`cloud/nexus-vault.ts` builds it). Here, in * one apply frame on the store: * * - **Rule 1** (`reconcile.ts`): a field an inherited capture or an * inherited sealed-but-unpushed op changed is re-emitted from the live row * at `tick(at_ms)` of that change. * - Every other field is compared per (table, uid, column) with the merged * state; a differing field is decided by {@link decideReconcileField}: * - **rule 2**, the local field HLC is below the merged one (a row with * no row meta, as a never-synced store merging in holds, is dated by its * genesis HLC, §1.2; a local value it loses is reported in `overwritten`): * the merged value is written locally in the apply frame, its intents * recorded so the sealer never sends it, and the merged field HLC stored * in the row meta. Nothing is emitted; * - **rule 3**, the local HLC is at or above the merged one (a value the * original replica sealed but the stream never saw): it is emitted in * the `rebind` frame with that HLC pinned (op `fh`, or `h` for a delete). * - The store takes the merged state's pull position, and the due record is * cleared, which lets push resume. * * Rows: a row only the merged state has is adopted (inserted) unless the * store holds a newer tombstone for it (rule 3: a pinned delete); a row the * merged state deleted is adopted (deleted) unless the store's row is newer * (rule 3: the whole live row re-inserted as an I whose op HLC is above the * tombstone, its field HLCs pinned; the merge engine voids a U on a * tombstoned row but re-creates the row from a newer I, T13392). A live row the merged state never knew, with * no inherited change, is emitted: with its row meta's HLCs pinned when it * has meta, else as a plain insert (nothing local is ever deleted that the * stream cannot account for). * * Reference columns are compared by the uid of the row they point at, and an * adopted reference is resolved to this store's local key ({@link resolveRef}). * A merged row whose reference does not resolve here, or whose insert hits a * local constraint, is left out and counted in `unresolved`. In a merge join * (T13466) a local constraint refuses the whole reconcile instead * ({@link MergeKeyCollisionError}, T13504). * * @task T13335 * @module store/sync/reconcile-copy */ import type { DatabaseSync } from 'node:sqlite'; import type { TableScope } from '@cleocode/contracts'; import type { LedgerWireValue } from '@cleocode/contracts/ledger'; import { type StreamCursor } from './pull.js'; import { type ReconcileReport } from './reconcile.js'; /** Options for {@link reconcileCopy}. */ export interface ReconcileCopyOptions { readonly scope: TableScope; /** The stream the merged state was pulled to the head of. */ readonly stream: string; /** The merged state's pull position at head: the store takes it. */ readonly mergedCursor: StreamCursor; /** Wall clock (ms). @defaultValue Date.now */ readonly now?: () => number; /** * The checkpoint as restored, before the pull (T13507). A merge join dates a * meta-less local row by its `updated_at`, which cannot show which fields it * touched; a stream field whose HLC moved past this baseline is a real write * since the cut and wins. */ readonly baseline?: DatabaseSync; /** * Adopt a merged row's secret columns with it (T13544). Set only when the * merged state's secret cells already hold this machine's values: the global * merge scratch, whose credential cells the checkpoint's sealed sidecar * filled under this machine's key. A row this store already holds keeps its * own secrets either way. @defaultValue false */ readonly adoptSecrets?: boolean; } /** What {@link reconcileCopy} did. */ export interface ReconcileCopyReport { /** Fields adopted from the merged state (rule 2), and rows inserted and deleted by adoption. */ readonly adoptedFields: number; readonly adoptedInserts: number; readonly adoptedDeletes: number; /** Rule-3 emits with pinned HLCs. */ readonly pinned: number; /** Merged rows left out: a reference that does not resolve here, or a local constraint. */ readonly unresolved: number; /** * Fields of a row this store held with no row meta (a merge join, T13466) * whose local value lost to a newer merged one: the local value was * replaced. Read the store's safety backup to recover one. */ readonly overwritten: ReadonlyArray<{ table: string; uid: string; column: string; }>; /** * The other direction (T13507): fields of such a row whose newer local value * replaced the stream's value, so every replica takes the local one. */ readonly replaced: ReadonlyArray<{ table: string; uid: string; column: string; }>; /** * Rows of a never-synced store that a newer stream delete removed (T13508). The store's * safety backup holds them. */ readonly deleted: ReadonlyArray<{ table: string; uid: string; }>; /** The `rebind` frame (rule 1 and rule 3). */ readonly frame: ReconcileReport; } /** A merged row a merge join cannot place: a row this store holds already has its local key (T13504). */ export interface MergeKeyCollision { readonly table: string; readonly uid: string; /** The merged row's local key values. */ readonly key: readonly LedgerWireValue[]; } /** * A merge join refused: a merged row's local key is held by this store's own * row, and nothing yet settles such a collision across devices, so emitting * this store's row would leave it held on every peer (T13504). The apply * frame rolls back: nothing is adopted or emitted, and the reconcile stays due. */ export declare class MergeKeyCollisionError extends Error { readonly collisions: readonly MergeKeyCollision[]; constructor(collisions: readonly MergeKeyCollision[]); } /** * Reconcile a rebound store against the merged state (module docs). Must * run outside a transaction. Does nothing, and returns null, when no * reconcile is due. * * @param db - The rebound store (the canonical store). * @param merged - The scratch store: the latest verified checkpoint pulled to head. Read-only here. * @param o - Scope, stream, the merged pull position and the clock. * @returns What was adopted and emitted, or null when nothing was due. */ export declare function reconcileCopy(db: DatabaseSync, merged: DatabaseSync, o: ReconcileCopyOptions): ReconcileCopyReport | null; /** * The merged rows a merge join (T13466) could not place: a live merged row * this store has no row for, whose local key a different row here already * holds. Read-only; run it before the join so a refusal changes nothing * (T13504). Tables whose key holds a reference are left to the reconcile's * own check, which refuses the same way. * * @param db - The store about to merge in. * @param merged - The merged state (checkpoint pulled to head). * @param scope - The store's scope. * @returns The colliding merged rows, in table order. */ export declare function mergeKeyCollisions(db: DatabaseSync, merged: DatabaseSync, scope: TableScope): MergeKeyCollision[]; //# sourceMappingURL=reconcile-copy.d.ts.map