/** * Own-echo sequencing and the foreign-touch index (T13193; journal spec §3.5 * Rules 2-3, R7-6). * * A sealed local transaction stays **unsequenced** until its own echo comes * back through the stream. Until then it keeps its undo (`_sync_undo`, the * values) and its row undo (`_sync_row_undo`, the merge state: row meta, * leaves, frontiers), so a scoped rebase can rewind it. * * When the echo applies, the **fast path** decides whether a rebase is needed: * if no foreign transaction applied after this transaction's commit touched * its rows, the stream order and the local order agree, so the echo is simply * sequenced and its undo dropped. The order is exact, with no clock: a local * transaction's position is its first capture's seq, and a foreign touch * records the capture sequence (`sqlite_sequence` of `_sync_capture`) when its * transaction applied. Both come from the one AUTOINCREMENT counter. * * Unframed writers (foreign connections) seal as singleton transactions with * no frame and undo rows with no `txn_local`: they have no position here, are * sequenced on echo without a check, and keep their undo until R-2 keys it by * capture seq. A split part-set (`LedgerTxn.part`, not sealed today) would map * one frame to several transactions. * * The index holds only entries at or after the oldest unsequenced local * transaction and is bounded by {@link FOREIGN_TOUCH_MAX}; past the bound it * is marked incomplete and the fast path declines (falling back to the scoped * rebase) until the unsequenced backlog drains. * * @module store/sync/sequencing * @task T13193 */ import type { DatabaseSync } from 'node:sqlite'; import { LedgerActor, LedgerOp, type LedgerWireValue } from '@cleocode/contracts/ledger'; import { type FieldStateSnapshot } from './field-leave.js'; import { type RowMetaFull } from './row-meta.js'; /** The foreign-touch index's bound (spec R7-6). */ export declare const FOREIGN_TOUCH_MAX = 200000; /** `_sync_meta` key set while the foreign-touch index overflowed its bound. */ export declare const FOREIGN_TOUCH_INCOMPLETE_KEY = "sync.foreign_touch_incomplete"; /** One row a transaction touched. */ export interface TouchedRow { readonly table: string; readonly uid: string; } /** Whether undo is being written (§3.5 Rule 2: on exactly while push is on). */ export declare function undoEnabled(db: DatabaseSync): boolean; /** * Snapshot a row's merge state before a local op moves it (the sealer calls * this per sealed op while undo is enabled): its row meta, leaves and * frontiers, so a rewind can restore them with the values. * * @param db - The store, inside the sealer's transaction. * @param txn - The sealed transaction id. * @param idx - The op's index in it. * @param tbl - The row's table. * @param uid - The row's uid before the op. * @param values - The values a rewind restores, when the op's own image * cannot supply them (a delete's secret columns, which never travel in it). */ export declare function snapshotRowUndo(db: DatabaseSync, txn: string, idx: number, tbl: string, uid: string, values?: Readonly>): void; /** * The capture position now: the last seq `_sync_capture` handed out (0 when * none). A write made after this point has a larger seq. */ export declare function capturePosition(db: DatabaseSync): number; /** * The oldest unsequenced local transaction's position: its frame's first * undo seq. Joins through the `_sync_txn_frame` index (T13260), since it * runs on every echo and foreign apply under the write lock. */ export declare const OLDEST_UNSEQUENCED_SQL = "SELECT min(u.seq) AS p FROM _sync_undo u JOIN _sync_txn t ON t.frame = u.txn_local\n WHERE t.state IN ('sealed', 'segmented') AND NOT EXISTS (SELECT 1 FROM _sync_sequenced s WHERE s.txn = t.txn)"; /** * Drop the undo of frames sealed into no transaction (netted away, or * inherited/folded) that hold no live capture. Probes `_sync_txn` through * the `_sync_txn_frame` index (T13260). */ export declare const DROP_NETTED_UNDO_SQL = "DELETE FROM _sync_undo WHERE txn_local IS NOT NULL\n AND NOT EXISTS (SELECT 1 FROM _sync_txn t WHERE t.frame = _sync_undo.txn_local)\n AND NOT EXISTS (SELECT 1 FROM _sync_capture c WHERE c.frame = _sync_undo.txn_local AND c.state = 'live')"; /** * Record the rows an applied foreign transaction touched, at `pos`. Nothing * is recorded while no local transaction is unsequenced (no echo will ask). * * @param db - The store, inside the apply frame. * @param rows - The rows the transaction wrote. * @param pos - {@link capturePosition} when the transaction began applying. */ export declare function recordForeignTouches(db: DatabaseSync, rows: readonly TouchedRow[], pos: number): void; /** `_sync_meta` key holding the foreign-touch index's row count. */ export declare const FOREIGN_TOUCH_COUNT_KEY = "sync.foreign_touch_count"; /** A sealed local transaction as the fast path sees it. */ export interface LocalTxn { readonly txn: string; readonly frame: string | null; /** Its first capture's seq, or null when it kept no undo. */ readonly position: number | null; } /** * The unsequenced sealed local transaction with this id, or null (not ours, * or already sequenced). * * @param db - The store. * @param txn - The transaction id the echo carries. * @returns The local transaction, or null. */ export declare function unsequencedLocalTxn(db: DatabaseSync, txn: string): LocalTxn | null; /** * The own-echo fast path (Rule 3): whether no foreign transaction applied at * or after `local`'s commit touched any of `rows`. False while the index is * incomplete. * * @param db - The store. * @param local - The echoed local transaction. * @param rows - The rows its ops wrote. * @returns True when the echo can be sequenced without a rebase. */ export declare function ownEchoFastPath(db: DatabaseSync, local: LocalTxn, rows: readonly TouchedRow[]): boolean; /** * Mark an own transaction sequenced by its echo: record it, drop its undo * and row undo (and the undo of frames that netted to no transaction), and * prune the foreign-touch index past the new oldest * unsequenced transaction (resetting it, and its incomplete mark, when none * is left). * * @param db - The store, inside the apply frame. * @param local - The echoed local transaction. * @param at - The echo's stream position and the time. */ export declare function markSequenced(db: DatabaseSync, local: LocalTxn, at: { readonly stream: string; readonly seq: number; readonly nowIso: string; /** `void` when the stream refused it: it stays rewound and keeps its undo (Rule 6). */ readonly outcome?: 'applied' | 'void'; }): void; /** One sealed, unsequenced local transaction, with its ops (a rebase's unit). */ export interface UnsequencedTxn extends LocalTxn { /** The frame's actor (`LedgerActor`), when it was JSON. */ readonly actor: LedgerActor | null; readonly actorOp: string | null; readonly ops: readonly LedgerOp[]; } /** * Every sealed local transaction not yet sequenced, in local commit order. * * @param db - The store. * @returns The transactions with their sealed ops. */ export declare function unsequencedLocalTxns(db: DatabaseSync): UnsequencedTxn[]; /** A local op's row undo: the merge state (and, once replayed, the values) before it. */ export interface RowUndo { readonly meta: RowMetaFull | null; readonly state: FieldStateSnapshot | null; /** Wire values before the op's last replay; null before its first. */ readonly values: Readonly> | null; } /** * The row undo of a sealed local op, or undefined when none was kept. * * @param db - The store. * @param txn - The local transaction. * @param idx - The op's index. * @returns The snapshot, or undefined. */ export declare function readRowUndo(db: DatabaseSync, txn: string, idx: number): RowUndo | undefined; /** * Re-snapshot a local op's row undo right before a rebase replays it: the * merge state and the values it is about to write over, so the next rewind * restores exactly what this replay sat on (D2). * * @param db - The store, inside the rebase frame. * @param txn - The local transaction. * @param idx - The op's index. * @param tbl - The row's table. * @param uid - The row's uid. * @param values - The wire values the op is about to overwrite. */ export declare function resnapshotRowUndo(db: DatabaseSync, txn: string, idx: number, tbl: string, uid: string, values: Readonly>): void; /** Undo a store may hold before it warns and schedules a rebind (§3.5 Rule 2, C1). */ export declare const UNDO_BUDGET_BYTES: number; /** The share of {@link UNDO_BUDGET_BYTES} at which `cleo doctor` and status warn. */ export declare const UNDO_BUDGET_WARN_RATIO = 0.8; /** * `_sync_meta` key set (to when) the first time undo reached its budget: the * persistent `undo_budget_exceeded` warning, and the rebind it schedules for * the next pull to head (D5). Only that rebind clears it. */ export declare const UNDO_BUDGET_EXCEEDED_KEY = "sync.undo_budget_exceeded"; /** How much undo a store holds against its budget (§3.5 Rule 2, D5). */ export interface UndoBudget { /** * Undo payload bytes (`_sync_undo` images plus `_sync_row_undo` snapshots), * by `octet_length()`: the stored size, read without decoding the text. */ readonly bytes: number; readonly budget: number; /** `warn` from 80%; `exceeded` at 100% or once it has been (persistent). */ readonly state: 'ok' | 'warn' | 'exceeded'; /** When undo first reached the budget, or null. A rebind is scheduled from then. */ readonly exceededAt: string | null; } /** * The store's undo against its budget. Read-only (a snapshot is fine). * Undo is never stopped: at 100% the replica keeps writing it, warns, and * rebinds at the next pull to head (D5). * * @param db - The store. * @param budget - The budget in bytes. @defaultValue UNDO_BUDGET_BYTES * @returns The bytes held, the budget and the warning state. */ export declare function undoBudget(db: DatabaseSync, budget?: number): UndoBudget; /** * {@link undoBudget}, persisting the `undo_budget_exceeded` warning the first * time undo reaches the budget (a pull's check; D5). * * @param db - The store, outside a transaction. * @param nowIso - Now (ISO-8601). * @param budget - The budget in bytes. @defaultValue UNDO_BUDGET_BYTES * @returns The budget state after recording. */ export declare function recordUndoBudget(db: DatabaseSync, nowIso: string, budget?: number): UndoBudget; //# sourceMappingURL=sequencing.d.ts.map