import type { ScopeCopyMoveConfirmation, ScopeScriptCopy } from './scope-host.js'; /** * The directory's copy ledger (#1722), in the one SQL grammar both adapters run: the pure * adapter's directory and the control-plane Durable Object build the same `scope_script_copies` * table (`lint:spine-ddl` holds them to it), so the statements that lease, confirm and claim * its entries live here rather than being spelled twice. * * A pending entry belongs to the move that recorded it until its lease runs out. Two things * read that lease, and they are what make crash recovery safe against a move that is merely * slow rather than dead: * * - The bind that routes the scope onto the move's destination (`COPY_MOVE_LIVE_PREDICATE`) * lands only while every entry of the move is pending, unclaimed and inside its lease, and * settles them in the same write (`COPY_MOVE_CONFIRM_SQL`). * - A sweep claims only an entry whose lease has run out (`COPY_EXPIRED_SQL` then * `COPY_CLAIM_SQL`), which no bind can confirm any more. * * So exactly one of them acts on a move: the bind before the lease ends, or the sweep after. */ /** Appended to a conditional bind's `UPDATE scopes … WHERE`. Params: `copyMoveLiveParams`. */ export declare const COPY_MOVE_LIVE_PREDICATE = "\n AND EXISTS (SELECT 1 FROM scope_script_copies AS move\n WHERE move.tenant_id = scopes.tenant_id AND move.scope_id = scopes.scope_id AND move.move_id = ?)\n AND NOT EXISTS (SELECT 1 FROM scope_script_copies AS move\n WHERE move.tenant_id = scopes.tenant_id AND move.scope_id = scopes.scope_id AND move.move_id = ?\n AND (move.state <> 'pending' OR move.lease_owner IS NOT NULL\n OR move.lease_until IS NULL OR move.lease_until <= ?))"; export declare const copyMoveLiveParams: (confirm: ScopeCopyMoveConfirmation, now: string) => string[]; /** * Run in the bind's own transaction once its row update landed: the destination is now the * route, and the source becomes the state the confirmation names, with the marker its export * read. An adopt or rebind names `eligible`, so the sweep wipes the old copy under its fence * (the old copy is no backout; rollback is a snapshot taken first). A carry names `retained` * and promotes the source itself once it has ruled out an overtaken destination. A source on a * deployment that cannot fence a wipe stays `retained`. Params: `copyMoveConfirmParams`. */ export declare const COPY_MOVE_CONFIRM_SQL = "\n UPDATE scope_script_copies SET\n state = CASE role WHEN 'destination' THEN 'done' ELSE ? END,\n load_stamp = CASE role WHEN 'destination' THEN load_stamp ELSE ? END,\n revision = CASE role WHEN 'destination' THEN revision ELSE ? END,\n last_attempt_at = ?\n WHERE tenant_id = ? AND scope_id = ? AND move_id = ? AND state = 'pending'"; export declare const copyMoveConfirmParams: (confirm: ScopeCopyMoveConfirmation, tenantId: string, scopeId: string, now: string) => (string | null)[]; /** Pending entries whose lease ran out before `?` (or that never had one), oldest first, `LIMIT ?`. */ export declare const COPY_EXPIRED_SQL = "\n SELECT tenant_id, scope_id, script_ref, move_id FROM scope_script_copies\n WHERE state = 'pending' AND (lease_until IS NULL OR lease_until <= ?)\n ORDER BY lease_until, tenant_id, scope_id, script_ref, move_id LIMIT ?"; /** Claim one expired entry, re-checked in the write. Params: owner, leaseUntil, the key, now. */ export declare const COPY_CLAIM_SQL = "\n UPDATE scope_script_copies SET lease_owner = ?, lease_until = ?\n WHERE tenant_id = ? AND scope_id = ? AND script_ref = ? AND move_id = ?\n AND state = 'pending' AND (lease_until IS NULL OR lease_until <= ?)"; /** The move id every backfilled entry carries (#1722), which names it as one in a listing. */ export declare const BACKFILL_MOVE_ID = "backfill:1722"; /** * Record a copy made before the ledger existed, as `retained` (#1722). Never pending, so no move's * confirmation and no lease sweep ever acts on it; never `eligible`, because a historic copy has * no recorded load stamp for a fenced wipe to compare. Refused under a reap claim, like any new * entry, and skipped when the ledger already names the script for this scope in any state: from * that entry on the ledger tracks that store, historic data in it included. * Params: `copyBackfillParams`. */ export declare const COPY_BACKFILL_SQL = "\n INSERT INTO scope_script_copies (tenant_id, scope_id, script_ref, move_id, state, last_attempt_at)\n SELECT tenant_id, scope_id, ?, ?, 'retained', strftime('%Y-%m-%dT%H:%M:%fZ','now') FROM scopes\n WHERE tenant_id = ? AND scope_id = ? AND reap_claimed_at IS NULL\n AND NOT EXISTS (SELECT 1 FROM scope_script_copies AS copy\n WHERE copy.tenant_id = scopes.tenant_id AND copy.scope_id = scopes.scope_id AND copy.script_ref = ?)"; export declare const copyBackfillParams: (tenantId: string, scopeId: string, scriptRef: string) => string[]; /** What a backfill insert did: wrote the entry, found the script already ledgered, or was refused. */ export type ScopeCopyBackfillResult = 'recorded' | 'ledgered' | 'reaping' | 'missing'; /** * Settle the scope's backfilled entries a tracked entry has taken over (#1722, review r1). The * backfill records a script only while no move names it; once a move's own entry for the same * script is `done`, that store was wiped under the move's fence or is the scope's route, so the * historic data the backfilled entry stood for is gone or reached. Leaving the entry `retained` * would send every later reap and erasure to a store that refuses writes behind its tombstone. * Run in the same transaction as every write that settles an entry `done`: a settle and a * confirming bind, on both adapters. Params: tenantId, scopeId. */ export declare const COPY_BACKFILL_SUPERSEDE_SQL = "\n UPDATE scope_script_copies SET state = 'done', last_attempt_at = strftime('%Y-%m-%dT%H:%M:%fZ','now')\n WHERE tenant_id = ? AND scope_id = ? AND move_id = 'backfill:1722' AND state <> 'done'\n AND EXISTS (SELECT 1 FROM scope_script_copies AS tracked\n WHERE tracked.tenant_id = scope_script_copies.tenant_id AND tracked.scope_id = scope_script_copies.scope_id\n AND tracked.script_ref = scope_script_copies.script_ref AND tracked.move_id <> 'backfill:1722'\n AND tracked.state = 'done')"; /** * The scope row read in the backfill insert's own transaction: what explains an insert that wrote * nothing, and the erasure epoch a recorded entry was written under. An erasure claimed before the * insert moved the epoch past what it compared (its `shredSubject` row's `before`), so a subject * whose erasure's epoch is below this one never reached the recorded copy, whatever order their * audit rows landed in. Params: tenantId, scopeId. */ export declare const COPY_BACKFILL_SCOPE_SQL = "SELECT reap_claimed_at, COALESCE(erasure_epoch, 0) AS erasure_epoch FROM scopes WHERE tenant_id = ? AND scope_id = ?"; /** * What an erasure's `shredSubject` row records in `before` (#1722): the erasure epoch it ran under * and how it ran. `orchestrated` claimed the scope's copy inventory at that epoch and redacted every * copy in it; `direct` took no claim and redacted only the scope's own store, so it never reaches a * script copy whatever its epoch. The backfill orders erasures against the copies it records by it. */ export interface ErasureEpochStamp { erasureEpoch: number; path: 'orchestrated' | 'direct'; } /** `COPY_BACKFILL_SCOPE_SQL`'s row. */ export interface CopyBackfillScopeRow { reap_claimed_at: string | null; erasure_epoch: number; } /** Read the outcome of a backfill insert that wrote nothing, from `COPY_BACKFILL_SCOPE_SQL`'s row. */ export declare const copyBackfillRefusal: (scope: CopyBackfillScopeRow | undefined) => ScopeCopyBackfillResult; /** * The fence a copy move's restore carries into the destination's own store (#1722). The vertical * that holds the destination has no reach to the directory, so the move's lease travels with * the restore instead: the store refuses the load, inside the load's own transaction, once * `notAfter` has passed, and writes nothing. * * `notAfter` sits a margin inside the move's lease (`copyRestoreFence`), and everything that could * make a late restore wrong waits for that lease to end first. A sweep claims an entry only once * its lease has run out. A reap claims the scope, and an erasure finalizes, only while no entry * of any move is pending, so for an unconfirmed move only after that sweep. A restore that is * not refused therefore committed before any of them, and one that would land after any of them * is refused. The margin absorbs clock skew between the directory and the destination, and * `moveId` and `erasureEpoch` name the move and the erasure state it was issued under. */ export interface CopyRestoreFence { moveId: string; notAfter: string; erasureEpoch: number; } /** The fence for a move whose entries were recorded no earlier than `recordedAt` (epoch ms). */ export declare const copyRestoreFence: (moveId: string, recordedAt: number, leaseMs: number, erasureEpoch: number) => CopyRestoreFence; /** Whether a restore carrying `fence` may no longer land at `now` (epoch ms). */ export declare const copyRestoreFenceLapsed: (fence: CopyRestoreFence, now: number) => boolean; /** The refusal a lapsed fence answers with, the same words on every host. */ export declare const COPY_RESTORE_FENCE_LAPSED = "the copy move's lease ran out before its restore; nothing was loaded (#1722)"; /** A ledger row as both adapters read it. */ export interface ScopeScriptCopyRow { tenant_id: string; scope_id: string; script_ref: string; move_id: string; state: string; load_stamp: string | null; revision: string | null; role: string | null; lease_until: string | null; } /** The columns `ScopeScriptCopyRow` names, for a read that does not take `SELECT *`. */ export declare const SCOPE_SCRIPT_COPY_COLUMNS = "tenant_id, scope_id, script_ref, move_id, state, load_stamp, revision, role, lease_until"; export declare const scopeScriptCopyOf: (r: ScopeScriptCopyRow) => ScopeScriptCopy; //# sourceMappingURL=scope-copy-ledger.d.ts.map