/** * Pull one stream's journal into the store (journal spec §3.1 "Receive", * §2.9; T12343 S5-1). * * {@link pullStream} pages through the stream after the store's persisted * `_sync_cursor` (the journal client verifies order, gaps, the replica-to- * device pin, trust, hashes and decryption before anything is returned), * and for each segment, in ONE local transaction per page: * - decodes it: `deflate-raw(canonical JSON of LedgerTxn[])`, parsed with * the ledger schema; a vault delta segment (`cleo-vault-delta/v1`, a count * correction a vault push appended before the stream's genesis) carries no * ops and is passed over; * - checks every transaction's signature against the segment's device key * (§2.8); one bad signature refuses the segment, and the pull stops there: * the segments before it are staged and applied, the segment and what * follows are not; * - skips every transaction id already staged from this stream * (`_sync_seen_txn`): a re-delivered transaction is never staged twice, so * a counter delta is never applied twice; * - refuses a transaction at or below its origin's floor that has no seen * row (`_sync_seen_floor`, T13318): an origin's transactions are first * delivered in `local_seq` order, so such a transaction was either pruned * from the seen ledger or arrived out of order; the pull stops there, loud, * and never skips it as seen; * - stages the rest (`stageTxns`), schema-ahead ones included (the applier * holds them `refused-schema` and retries them on every apply), and * records each `retire` at its stream seq (`_sync_retired`, T13278); * - advances the cursor. * * Then the applier applies what is staged, in stream order, own echoes * included (they feed the fast path and the scoped rebase, §3.5). A pull * that reached the head runs the rebind the undo budget scheduled (D5, * `rebind.ts`), when the caller passes the store's open options. * * The page source is a port ({@link SegmentPuller}): the journal client in * production (`cloud/nexus-vault.ts`), a fake in tests. * * @task T12343 * @module store/sync/pull */ import type { DatabaseSync } from 'node:sqlite'; import type { TableScope } from '@cleocode/contracts'; import { LedgerTxn } from '@cleocode/contracts/ledger'; import { type ApplyReport, type ApplyStagedOptions } from './apply/applier.js'; import { type RebindAtHeadReport } from './rebind.js'; import type { SyncOpenOptions } from './replica.js'; import { type ServerRetirement } from './retire.js'; /** Where a pull stands (the journal client's `PullCursor`). */ export interface StreamCursor { /** The last stream seq staged. */ readonly after: number; /** Whether `replicas` lists every replica with a segment at or below `after`. */ readonly knowsAllReplicas: boolean; /** Per replica: the device pinned to it and its last replicaSeq staged. */ readonly replicas: Readonly>; } /** One verified, decrypted segment, as the journal client returns it. */ export interface PulledStreamSegment { readonly seq: number; readonly replicaId: string; readonly replicaSeq: number; readonly deviceId: string; readonly plaintext: Uint8Array; readonly schemaVersion: number; } /** Pulls the next verified page after `cursor`. */ export type SegmentPuller = (cursor: StreamCursor) => Promise<{ readonly segments: readonly PulledStreamSegment[]; readonly cursor: StreamCursor; readonly head: number; }>; /** The index of a segment's first transaction its device did not sign, or null when every one is signed. */ export type TxnVerifier = (deviceId: string, txns: readonly LedgerTxn[]) => number | null; /** The plaintext kind of a vault count-delta segment (`cloud/nexus-vault.ts`). */ export declare const VAULT_DELTA_KIND = "cleo-vault-delta/v1"; /** Options for {@link pullStream}. */ export interface PullStreamOptions { readonly scope: TableScope; readonly stream: string; /** This store's bound replica (own echoes are recognised by it). */ readonly replica: string; readonly pull: SegmentPuller; readonly verify: TxnVerifier; /** The cursor to start from when the store has none persisted (the checkpoint it was cut from or restored). */ readonly initialCursor: StreamCursor; /** Wall clock. @defaultValue Date.now */ readonly now?: () => number; /** Seals pending local captures before a rebase (the applier's `seal`). */ readonly seal: ApplyStagedOptions['seal']; /** Applier options passed through (pages, budget). */ readonly apply?: Partial>; /** * Opens the staged sealed secrets (§2.7, T13467): unseals each and * re-encrypts it under this machine's key, AFTER staging and BEFORE the * apply frame (no crypto inside a frame, H1). Without it, secret ops hold. */ readonly openSecrets?: () => Promise>; /** Environment for the `sync.pull` kill switch. @defaultValue process.env */ readonly env?: NodeJS.ProcessEnv; /** * After staging, drop each origin's seen-txn rows below its staged floor * ({@link pruneSeenTxns}, T13318). A pruned transaction delivered again is * refused (`below-floor`), never applied twice. @defaultValue false */ readonly pruneSeen?: boolean; /** * The canonical store's open options: when given and the undo budget * scheduled a rebind (`sync.undo_budget_exceeded`, D5), a pull that reaches * the stream's head runs it ({@link rebindAtHead}, T13278). A pull that is * refused or stops short never does, so the key stays set. */ readonly rebind?: Omit; /** * The server's retirements on this stream (T13366): after staging and * before the apply, each recorded retire they match is confirmed * ({@link confirmRetirements}). A retire never confirmed changes nothing. */ readonly serverRetirements?: readonly ServerRetirement[]; } /** * Why {@link pullStream} stopped short, as a kind callers branch on (the * message in `refused` is for people and may be reworded): * - `pull-off`: `sync.pull` is off, nothing was pulled; * - `segment`: a segment is malformed, carries a transaction its device did * not sign, or a transaction that is not its replica's; * - `below-floor`: a segment carries a transaction at or below its origin's * staged floor with no seen row: pruned from the seen ledger, or delivered * out of `local_seq` order (T13318). */ export type PullRefusal = 'pull-off' | 'segment' | 'below-floor'; /** What {@link pullStream} did. */ export interface PullStreamReport { readonly stream: string; /** * Why the pull stopped short, or null: `sync.pull` off (nothing pulled), or * a refused segment (a malformed body or a transaction its device did not * sign), naming its seq, replica and device. Everything before a refused * segment was staged and applied, and the cursor stops just before it. */ readonly refused: string | null; /** The kind of `refused` ({@link PullRefusal}), or null when nothing was refused. */ readonly refusedKind: PullRefusal | null; /** Segments received, vault deltas passed over, transactions staged, re-deliveries skipped. */ readonly segments: number; readonly vaultDeltas: number; readonly staged: number; readonly redelivered: number; /** The stream position the store has staged up to, and the server's head. */ readonly after: number; readonly head: number; /** What the applier did, or null when the pull was refused. */ readonly apply: ApplyReport | null; /** The rebind this pull ran at head (T13278), or null. */ readonly rebind: RebindAtHeadReport | null; } /** A segment the store refuses to stage (a malformed body or a bad transaction signature). */ export declare class SegmentRefusedError extends Error { readonly code = "E_SYNC_SEGMENT_REFUSED"; } /** The store's persisted cursor for `stream`, or null. */ export declare function readStreamCursor(db: DatabaseSync, stream: string): StreamCursor | null; /** * Persist the store's pull position for `stream`, in the caller's * transaction (the pull's page, or a JOIN seeding it from the checkpoint it * restored). * * @param db - The store. * @param stream - The stream. * @param cursor - The position. * @param nowIso - The time. */ export declare function writeStreamCursor(db: DatabaseSync, stream: string, cursor: StreamCursor, nowIso: string): void; /** * The size of `_sync_seen_txn` (T13317), for `cleo cloud status`: rows per * stream and the estimated payload bytes (stream and txn text plus the seq * integer, without SQLite's page overhead). Read-only, and O(origins): it * sums the per-origin counts `_sync_seen_floor` keeps with every insert and * prune (T13318), never the ledger itself. * * @param db - The store. * @returns Zeroes when the tables are not installed; the ledger walked when * only the floor table is missing. */ export declare function seenTxnReport(db: DatabaseSync): { rows: number; bytes: number; byStream: Record; }; /** * Drop each origin's seen-txn rows of `stream` below its staged floor, * keeping `_sync_seen_txn` bounded (§3.1, T13318), and record how far each * origin was pruned (`pruned_upto`). Safe because an origin's transactions * are first delivered in `local_seq` order and the pull refuses any * transaction at or below the floor that has no seen row (`below-floor`): * a pruned transaction delivered again is refused loudly, never applied * twice. The row at the floor itself is kept, so re-delivery of the latest * transaction is still skipped quietly. * * @param db - The store, outside a transaction. * @param stream - The stream. * @returns How many rows were dropped. */ export declare function pruneSeenTxns(db: DatabaseSync, stream: string): number; /** * Pull and stage a stream's new segments, then apply (module docs). Must run * outside a transaction. A refused segment stops the pull: everything * before it is staged and applied, the cursor stops just before it, and the * report's `refused` names it (T13307), so the next pull reaches it again * without re-staging anything. * * @param db - The store. * @param o - {@link PullStreamOptions}. * @returns What was received, staged and applied. */ export declare function pullStream(db: DatabaseSync, o: PullStreamOptions): Promise; //# sourceMappingURL=pull.d.ts.map