/** * Optimistic-concurrency primitives for task rows (T12503). * * A task's version is its `updatedAt` timestamp, falling back to `createdAt` * for a row that has never been updated. Two rules make that timestamp usable * as a version: * * 1. Every guarded write computes its new `updatedAt` INSIDE the write * transaction with {@link nextTaskVersion}, which is strictly greater than * the stored value. Two writes in the same millisecond therefore never * leave the row at a version a stale reader already holds (no ABA). * 2. The comparison ({@link assertTaskVersion}) runs after `BEGIN IMMEDIATE` * holds the database write lock, so no other process can commit between * the check and the write. The field-level chokepoint * (`updateTaskFields`) additionally puts the version in the UPDATE's * WHERE clause, so a guarded write is a single compare-and-set statement. * * A conflict is raised by {@link taskConflictError}: `E_CONFLICT` with the * current version, the fields that differ from the caller's read, the stored * values to merge against, and a `--if-match` retry hint. * * `withWriteRetry` only absorbs `SQLITE_BUSY`; it cannot detect a lost update. * These helpers are what turn a stale read-modify-write into a typed * `E_CONFLICT` instead of a silent overwrite. * * @task T12503 * @epic T12497 */ import type { Task, TaskConflictChange } from '@cleocode/contracts'; import { CleoError } from '../errors.js'; /** The minimal row shape that carries a task version. */ export interface TaskVersionSource { /** Last-update timestamp (ISO 8601), or null/undefined for a never-updated row. */ updatedAt?: string | null; /** Creation timestamp (ISO 8601); the version of a never-updated row. */ createdAt?: string | null; } /** * Return the version of a task row: `updatedAt`, else `createdAt`, else `''`. * * @param row - A task or task row. * @returns The opaque version string callers pass back as `expectedUpdatedAt`. * @example * ```ts * const version = taskVersion(await accessor.loadSingleTask('T1')); * ``` */ export declare function taskVersion(row: TaskVersionSource | null | undefined): string; /** * Compute the `updatedAt` for a write on top of `current`: now, or 1 ms past * the stored version when the clock has not moved past it. * * @param current - The row as read inside the write transaction. * @param now - The candidate timestamp; defaults to the current time. * @returns An ISO timestamp strictly greater than the stored version. * @example * ```ts * row.updatedAt = nextTaskVersion(current); * ``` */ export declare function nextTaskVersion(current: TaskVersionSource | null | undefined, now?: string): string; /** * List the fields whose stored value differs from the caller's read. * * The version field (`updatedAt`) is never listed; an absent field and an * explicit `null` compare equal. * * @param baseline - The task as the caller read it. * @param current - The task as stored now. * @returns One entry per differing field, sorted by field name. * @example * ```ts * diffTaskFields(before, after); // [{ field: 'labels', was: '["a"]', now: '["a","b"]' }] * ``` */ export declare function diffTaskFields(baseline: Task, current: Task): TaskConflictChange[]; /** * Build the `E_CONFLICT` error for a stale task write. * * @param taskId - Task being written. * @param expectedUpdatedAt - The version the caller expected. * @param current - The row as stored now; a full task adds the merge summary. * @param baseline - The task as the caller read it, when the write path has it. * @returns A {@link CleoError} with `ExitCode.VERSION_CONFLICT` (LAFS * `E_CONFLICT`), a fix hint, and {@link TaskConflictDetails} as `details`. * @example * ```ts * throw taskConflictError('T1', expected, await accessor.loadSingleTask('T1'), baseline); * ``` */ export declare function taskConflictError(taskId: string, expectedUpdatedAt: string, current: Task | TaskVersionSource | null, baseline?: Task): CleoError; /** * Throw `E_CONFLICT` when the caller's expected version no longer matches. * * A no-op when `expectedUpdatedAt` is undefined, which preserves the * last-writer-wins behaviour of unguarded callers. * * @param taskId - Task being written. * @param current - The row as read inside the write transaction. * @param expectedUpdatedAt - The version the caller read, if it supplied one. * @param baseline - The task as the caller read it; enables the changed-field summary. * @throws {CleoError} `ExitCode.VERSION_CONFLICT` (LAFS `E_CONFLICT`) with * {@link TaskConflictDetails} as `details`. * @example * ```ts * assertTaskVersion('T1', current, guard?.expectedUpdatedAt, guard?.baseline); * ``` */ export declare function assertTaskVersion(taskId: string, current: Task | TaskVersionSource, expectedUpdatedAt: string | undefined, baseline?: Task): void; //# sourceMappingURL=task-version.d.ts.map