/** * The applier loop (T12344; journal spec §3.1, §3.2, §2.9). * * {@link applyStagedTxns} drains a stream's inbox in `(seq, txn_idx)` order. * Each transaction is decided and applied in ONE apply frame, together with * its status, its conflict records, its row meta and its leaves: * * 1. **Schema.** A segment or transaction format newer than this build is * `refused-schema` (E_SCHEMA_AHEAD). Every call re-opens the stream's * `refused-schema` transactions first, so one applies as soon as the * build or the store's schema has what it needs (T13719). * 2. **Skew (§1.3).** A transaction whose HLC is beyond the skew bound is * `held-skew`, and so is every later transaction of the same origin * replica in this pass (FIFO); other replicas keep flowing. * 3. **Dependency holds.** A transaction touching a row a pending * transaction writes waits (`pending`) behind it. Holds are scoped to * those rows, never head-of-line. * 4. **Plan** (nothing written). Each op is decided against the store's * state carried forward inside the transaction. The transaction is * `pending` when an op updates a row never seen, re-keys a row never * seen, references a target never seen (§3.2 NEW-2: no age limit), or * carries a secret (not unsealed yet); `refused-schema` when an op names * an unknown table or column, or is malformed. * 5. **Apply**, op by op, each against the store as it is now and inside its * own savepoint: * - references travel as target uids and are written as the targets' * local keys ({@link resolveRef}, following `tasks_uid_aliases`); a * reference to a deleted target is a `dangling-ref` conflict and a * revivable void, never pending; * - a guard trigger or constraint that aborts the write rolls back only * that op: a `guard` conflict and a void (§3.2 D, T12777); * - a parent delete whose CASCADE sync-set children remain follows the * table's `onRemoteParentDelete` policy: `conflict` voids it with a * `delete-with-live-children` conflict, `cascade-with-ops` deletes the * children first with tombstones; * - a K op re-keys the row through the write API (its meta and typed-rule * state move with it), after `remapPending` moves local captures and * unsent ops onto the new uid; a re-key onto a live uid is a * `uid-collision` void. * The effects go through the write API, which records the apply intents, * so the sealer echoes nothing; row meta is set to the engine's state, * conflicts are recorded, and the transaction is marked `applied`, * `conflict` (something recorded) or `void` (every op refused). * A late transaction of a replica retired earlier in the stream is * inherited history (§3.5 D5, T13278): applied the same way, but its merge * conflicts are not recorded against the successor's reconcile. * * Passes repeat while a pass applies something, so a transaction pending on * a row a later transaction inserts applies in the same call. * * Deletes run with foreign keys ON: the origin journals cascaded child Ds * before the parent's D, so a child D whose row is already gone applies as a * tombstone, never a missing-row failure; and SET NULL actions are not * journaled yet (T13226), so this replica's own FK actions fill that gap * (the delete records their intents, so nothing is re-emitted). * * Out of this slice: Gate C validators (PR-5), soft references across * streams (`_sync_soft_ref`), holds that follow a re-keyed uid, and the * scoped rebase (§3.5). * * @module store/sync/apply/applier * @task T12344 */ import type { DatabaseSync } from 'node:sqlite'; import type { TableScope } from '@cleocode/contracts'; import type { LedgerWireValue } from '@cleocode/contracts/ledger'; import { type CaptureTableDef } from '../capture.js'; import { type RowState } from '../merge/types.js'; import { type UndoBudget } from '../sequencing.js'; import { type ApplyWriteApi } from './write-api.js'; /** How {@link applyStagedTxns} runs. */ export interface ApplyStagedOptions { readonly scope: TableScope; readonly stream: string; /** This replica's id: its clock receives every applied transaction. */ readonly replica: string; /** The wall clock in ms; defaults to `Date.now`. */ readonly now?: () => number; /** * Seal this replica's pending captures first (§3.2 step 1), so the row * meta the merge reads covers every local write. */ readonly seal?: () => void; /** At most this many passes; defaults to 16. */ readonly maxPasses?: number; /** Ops per page; defaults to {@link REBASE_PAGE_OPS}. */ readonly pageOps?: number; /** A page stops taking transactions after this many ms; defaults to {@link REBASE_PAGE_MS}. */ readonly pageMs?: number; /** The undo budget in bytes; defaults to 256 MiB (`UNDO_BUDGET_BYTES`). */ readonly undoBudgetBytes?: number; /** * Secret values the receiver unsealed and re-encrypted under its own machine * key BEFORE the frame (§2.7, H1; T13467), keyed by {@link openedSecretKey}. * An op whose sealed secret has no entry here stays pending. */ readonly secrets?: ReadonlyMap; } /** * The key of one opened secret: its table, row uid, column and the sealed * envelope it was opened from (so a later re-seal of the column never reuses * a stale opening). */ export declare function openedSecretKey(table: string, uid: string, column: string, sealed: string): string; /** What one {@link applyStagedTxns} call left in the inbox, per status. */ export interface ApplyReport { readonly applied: number; readonly conflict: number; readonly void: number; readonly pending: number; readonly heldSkew: number; readonly refusedSchema: number; /** Conflict records written. */ readonly conflicts: number; readonly passes: number; /** Transactions applied inside a scoped rebase frame (§3.5 Rule 2). */ readonly rebased: number; /** Local transactions this call left held: a replay the stream refused (§3.5 Rule 5). */ readonly held: readonly string[]; /** Undo against its budget; `exceeded` is the persistent warning every pull carries (D5). */ readonly undoBudget: UndoBudget | null; /** Why nothing was applied (PAC-15 apply preconditions), when so. */ readonly blocked?: string; } /** * The row's state as the merge sees it: the live row with its field HLCs * and leaves, a tombstone, or never seen. A live row with no row meta (not * yet sealed) carries the genesis HLC, so any stream write wins over it. * * @param db - The store. * @param api - The write API (only its reads are used). * @param def - The table's capture definition. * @param uid - The row uid. * @param localReplica - This store's replica (the genesis floor). * @returns The row state. */ export declare function loadRowState(db: DatabaseSync, api: Pick, def: CaptureTableDef, uid: string, localReplica: string): RowState; /** A page holds at most this many ops (§3.5 Rule 3), unless one transaction is larger. */ export declare const REBASE_PAGE_OPS = 2000; /** A page stops taking transactions after this long (§3.5 Rule 3). */ export declare const REBASE_PAGE_MS = 50; /** * Apply a stream's staged transactions (see the module doc). * * @param db - The store (the journal schema applied, capture on); must not * be inside a transaction. * @param opts - Scope, stream, this replica, clock, sealing and pass bound. * @returns What the inbox holds for the transactions tried, per status. * * @example * ```ts * stageTxns(db, stream, segment, new Date().toISOString()); * const r = applyStagedTxns(db, { scope: 'project', stream, replica }); * if (r.pending > 0) reportHolds(); * ``` */ export declare function applyStagedTxns(db: DatabaseSync, opts: ApplyStagedOptions): ApplyReport; //# sourceMappingURL=applier.d.ts.map