/*! * Copyright (c) 2026 Interop Alliance. All rights reserved. */ /** * The RxDB conflict handler for a mutable-head (LWW) collection. RxDB's default * handler always drops the local fork and keeps the remote master, which is * correct for content-addressed (immutable-per-id) collections but wrong here: * every entity is a mutable head that two devices can edit concurrently, so a * genuine content conflict must be settled by last-write-wins on the payload's * own `updatedAt` (writerId tiebreak) -- exactly the rule two offline replicas * apply independently to converge. * * The wrinkle is that the conflicting bodies are EDV envelopes (ciphertext), so * `resolve` must decrypt both sides through this collection's cipher before it * can compare the plaintext `updatedAt` / `writerId`. `isEqual` stays cheap and * synchronous (a structural compare of the opaque bodies), as RxDB requires. * * Convergence: the server holds ONE winner of the push race as `realMasterState`; * every replica compares that same master against its own local edit, and the * `payloadWins` comparator is a total order over `(updatedAt, writerId)`, so the * globally-latest payload wins on every replica with no coordination. * * The resolution rules, in order: * * 1. Version-only conflict: when the real master's whole content (`data` + * `custom` + `_deleted`) still equals the assumed master's, the server holds * nothing newer than what this replica last synced -- the 412 came from a * stale `If-Match` (typically our own earlier write racing its feed echo). * The local state (edit or tombstone) is re-asserted and re-pushed against * the corrected version. Without this rule a local delete would be dropped by * rule 5 and the entity would silently resurrect. `custom` MUST be part of * this comparison, or a concurrent metadata-only edit committed on the server * would be misclassified here and clobbered (rule 2 is what settles it). * 2. Metadata conflict: `data` and `_deleted` are unchanged from the assumed * master but `custom` differs -- a metadata-only edit committed on the server * since this replica last synced (another device won the `/meta` race). * Metadata carries no LWW timestamp of its own (the payload `updatedAt` lives * in the encrypted `data`, which is equal on both sides here), so there is no * payload to compare; the sound, replica-independent default is that the * server-committed state wins -- the real master is adopted for `custom`. * Without this rule the equal-`data` case would fall through to rule 3, where * the two payloads compare equal and the tie keeps the local (stale) `custom`, * silently clobbering the committed metadata. * 3. An UNDECRYPTABLE side (the decrypt threw -- e.g. an envelope written under * a key epoch this device has not seen) is never scored as the loser: it is * presumed newer, not absent. An undecryptable master is adopted (never * re-pushed over with the possibly-older local payload); an undecryptable * local row is re-asserted (the user's edit is not silently dropped); both * undecryptable adopts the master (deterministic and convergent). Each case * is logged -- distinguishable from the intended tombstone/absent-body * `null` the remaining rules were written for. * 4. Both sides carry an LWW payload: pure payload LWW via `payloadWins`. * 5. A live local edit vs an incomparable remote (e.g. a remote tombstone): * the edit wins and is re-pushed (resurrection). * 6. Everything else -- a local tombstone vs a REAL remote content change, or * both sides incomparable: the master wins. Together with rule 5 this makes * the delete-vs-concurrent-edit rule "the edit wins" on every replica: a * tombstone carries no LWW payload of its own, so a genuine racing edit * deterministically survives, whichever write reached the server first. */ import type { WithDeleted } from 'rxdb/plugins/core'; import type { Json, SyncedDoc } from './types.js'; import { type LwwFields } from './lww.js'; /** * Builds an RxDB conflict handler that settles content conflicts by payload LWW, * decrypting through the supplied per-collection `decrypt`. * * @param decrypt {(envelope: Json) => Promise} this collection's decrypt * @param [payloadWins] {(remote: LwwFields, local: LwwFields) => boolean} * the total-order comparator deciding whether the remote payload replaces the * local one; defaults to {@link remotePayloadWins} (later `updatedAt` wins, * `writerId` breaks a tie) * @returns {import('rxdb/plugins/core').RxConflictHandler} */ export declare function makeLwwConflictHandler(decrypt: (envelope: Json) => Promise, payloadWins?: (remote: LwwFields, local: LwwFields) => boolean): { isEqual(a: WithDeleted, b: WithDeleted): boolean; resolve({ realMasterState, newDocumentState, assumedMasterState }: { realMasterState: WithDeleted; newDocumentState: WithDeleted; assumedMasterState?: WithDeleted; }): Promise>; }; //# sourceMappingURL=lwwConflictHandler.d.ts.map