/** * Application-facing multi-operation transactions. * * ```ts * const result = await db.transaction(async tx => { * tx.collection('users').set('u1', user); * tx.collection('orders').set('o1', order); * tx.collection('audit').set('a1', event); * }); * ``` * * Either every operation commits or none do. * * The callback **stages** operations; it does not write them. Nothing reaches * the authority until the callback returns, at which point the whole * transaction is sent as one request and committed as one durable record. That * is what makes the guarantee real: there is no window in which some of the * operations have been written and others have not. * * FeltDB does not implement a second transaction mechanism in TypeScript. This * builder produces a request for the store's own atomic transaction path, and * a runtime that cannot provide that path refuses the transaction rather than * silently degrading it into a sequence of independent writes. */ /** * What must be true of one record for the transaction to commit. * * Evaluated by the authority inside the same atomic boundary as the writes. * A client-side check cannot provide this: between the check and the write, * another writer commits. * * Every predicate is optional and only a supplied one is checked. `version` is * the record's `__version` -- the field you read back and pass to * `updateIfVersion` -- and it is owned by the writer, so a transaction that * wants to exclude its own successor must write an advanced `__version`. */ export interface AtomicTransactionPrecondition { collection: string; id: string; /** The record must not exist. */ requireAbsent?: boolean; /** * The record's current `__version`, spelled as `Collection.updateIfVersion` * spells it. * * `ifVersion` and `expectedVersion` are the same predicate; `ifVersion` is * the public spelling and `expectedVersion` is what goes on the wire. * Supplying both with different values is an error rather than a silent * preference, because a caller who wrote two different numbers did not mean * either of them. */ ifVersion?: number; /** The record's current `__version`. Wire spelling of `ifVersion`. */ expectedVersion?: number; /** The record's authority epoch. */ expectedEpoch?: number; /** The id of an unexpired lease the caller believes it holds. */ expectedLeaseId?: string; } /** Predicates that may be attached to a staged write. */ export interface AtomicTransactionGuard { requireAbsent?: boolean; /** Public spelling of `expectedVersion`; the two are one predicate. */ ifVersion?: number; expectedVersion?: number; expectedEpoch?: number; expectedLeaseId?: string; } /** One staged operation. An absent value means delete. */ export interface AtomicTransactionOperationRequest { collection: string; id: string; value?: Record; /** When set, the record must not already exist for the transaction to commit. */ requireAbsent?: boolean; /** Public spelling of `expectedVersion`; the two are one predicate. */ ifVersion?: number; /** Fence this write on the record's current `__version`. */ expectedVersion?: number; expectedEpoch?: number; expectedLeaseId?: string; } /** * A transaction described in full, rather than staged through a callback. * * The same request the builder produces, taking the same path to the same * authority call. It exists because a caller that already knows every operation * — because it read state, decided, and is now committing that decision — has * nothing to stage. * * ```ts * await db.transaction({ * transactionId, * preconditions: [{ collection: 'sessions', id, ifVersion: version }], * operations: [ * { collection: 'sessions', id, value: nextSession }, * { collection: 'changes', id: changeId, value: change }, * ], * }); * ``` */ export interface AtomicTransactionDocument { transactionId?: string; preconditions?: AtomicTransactionPrecondition[]; operations: AtomicTransactionOperationRequest[]; } export interface AtomicTransactionRequest { transactionId: string; operations: AtomicTransactionOperationRequest[]; /** * Conditions on records the transaction need not write. Read/decide/write * fencing routinely guards on a record it does not modify -- a session * generation, an ownership epoch -- and expressing that as a no-op write * would make the guard a mutation. */ preconditions?: AtomicTransactionPrecondition[]; } export interface AtomicTransactionResult { /** Durable identity of the transaction. Stable across restart and replay. */ transactionId: string; /** True when this id was already committed; nothing was applied again. */ duplicate: boolean; operations: number; stateBefore: number; stateAfter: number; /** Authority revision before and after this atomic commit. */ baseRevision: number; commitRevision: number; /** Stable revision and operation identities; never aliases of transactionId. */ revisionId: string; operationIds: string[]; } /** Staging surface for one collection inside a transaction. */ export interface AtomicTransactionCollection { /** Stage a write. */ set(id: string | number, value: T, options?: AtomicTransactionGuard): AtomicTransactionCollection; /** * Stage a write, in the argument order used by `Collection.insert`. * * Identical to `set` with the arguments swapped, so code that reads like the * rest of the collection API does not have to change shape inside a * transaction. */ insert(value: T, id: string | number, options?: AtomicTransactionGuard): AtomicTransactionCollection; /** Stage a delete. */ delete(id: string | number, options?: AtomicTransactionGuard): AtomicTransactionCollection; } /** The handle passed to a transaction callback. */ export interface AtomicTransactionScope { /** The transaction's durable identity, available while staging. */ readonly transactionId: string; collection(name: string): AtomicTransactionCollection; /** * Require something of a record without writing it. * * ```ts * tx.require({ collection: 'sessions', id: 's1', expectedVersion: 12 }); * ``` */ require(condition: AtomicTransactionPrecondition): AtomicTransactionScope; /** Operations staged so far. */ readonly operations: readonly AtomicTransactionOperationRequest[]; /** Preconditions staged so far. */ readonly preconditions: readonly AtomicTransactionPrecondition[]; } export interface AtomicTransactionOptions { /** * Supply an identity to make the commit idempotent: retrying with the same * id after an uncertain outcome cannot apply the transaction twice. */ transactionId?: string; } /** Thrown when a transaction cannot be committed. Nothing was applied. */ export declare class AtomicTransactionError extends Error { readonly transactionId: string; readonly code?: string; readonly status?: number; constructor(message: string, transactionId: string, code?: string, status?: number, cause?: unknown); } /** Which predicate failed, and what the authority actually holds. */ export interface ConditionalConflictDetail { predicate: 'version' | 'epoch' | 'lease' | 'present' | 'missing'; collection: string; key: string; expected?: number | string; actual?: number | string | null; } /** * A transaction was refused because a precondition did not hold. * * Distinct from every other failure a transaction can have, and carried as a * type rather than a message: a conflict means re-read and decide again, while * an authentication, network, validation or server error means something else * entirely, and a caller must not have to parse prose to tell them apart. * * A conflict guarantees that **zero** operations committed. */ export declare class ConditionalConflictError extends AtomicTransactionError { readonly code: "PRECONDITION_FAILED"; readonly failure?: ConditionalConflictDetail; constructor(message: string, transactionId: string, failure?: ConditionalConflictDetail); } /** * A refusal a conditional write is *allowed* to have, told apart from every * other way a commit can fail. * * The conditional collection helpers report a lost race as a value and raise * everything else. That distinction only holds if it is made in one place: a * runtime that reports a conflict differently from another would make * `putIfAbsent` mean one thing on a file runtime and another against a managed * authority, which is the defect these helpers exist to remove. * * Three shapes reach here, and all three are the same event: * * - an embedded runtime's `EmbeddedTransactionRejected`, which carries the * code and the structured failure directly; * - the direct authority's refused `POST /transactions`, whose body is carried * on the error as `feltdbCode` and `feltdbConflict`; * - a managed authority's refused `POST /v1/transactions`, which answers with * HTTP 409 and a `FeltDBServiceError` coded `PRECONDITION_FAILED` or * `CONFLICT`. * * Anything else -- authentication, schema validation, transport, a 5xx -- is * deliberately *not* a conflict. Converting one of those into "someone else * won the race" is how a caller silently skips work it was required to do. */ export declare function conditionalRefusal(error: unknown): { conflict: true; failure?: ConditionalConflictDetail; } | { conflict: false; }; /** * Normalise a precondition to the single wire field. * * `ifVersion` is the public spelling and `expectedVersion` is the wire's. They * are one predicate, so two different values for it is a caller error. */ export declare function normalisePrecondition(condition: AtomicTransactionPrecondition): AtomicTransactionPrecondition; /** Collects operations without performing any I/O. */ export declare class AtomicTransactionBuilder implements AtomicTransactionScope { readonly transactionId: string; private readonly staged; private readonly required; private sealed; constructor(transactionId?: string); get operations(): readonly AtomicTransactionOperationRequest[]; collection(name: string): AtomicTransactionCollection; get preconditions(): readonly AtomicTransactionPrecondition[]; require(condition: AtomicTransactionPrecondition): AtomicTransactionScope; /** Freeze the builder and produce the request to send. */ seal(): AtomicTransactionRequest; } //# sourceMappingURL=transaction.d.ts.map