CRUMB a card from devarno-cloud

CRDT Merge Strategies

stratt advanced 7 min read

ELI5

When two editors change the same unit, STRATT looks up which field they touched and applies that field’s house rule: meta uses last-write-wins, contracts demand a human, lists union, and the fingerprint is always recomputed at the end.

Technical Deep Dive

Strategy Routing

flowchart TB
E["applyEdit(doc, edit)"] --> R{"field type"}
R -->|"meta"| LWW["last_write_wins"]
R -->|"contract"| MAN["manual_resolution"]
R -->|"steps"| APP["append_wins"]
R -->|"imports"| UNI["union_merge"]
R -->|"status"| HRW["highest_restriction_wins"]
R -->|"id, slug, created"| IMM["immutable (reject)"]
LWW --> FP["recompute_post_merge<br/>→ new fingerprint"]
APP --> FP
UNI --> FP
HRW --> FP
MAN --> FP

Strategy Catalogue (packages/crdt/src/strategies.ts)

StrategyApplies ToBehaviour
last_write_winsmeta fieldsLatest timestamp wins
manual_resolutioncontractSurfaces a conflict; blocks publish until resolved
append_winscomposition.stepsBoth edits append; order preserved
union_mergeimportsSet union; duplicates collapsed
highest_restriction_winsstatusPicks most restrictive per AC-02 ordering
recompute_post_mergemeta.fingerprintAlways runs last; never user-supplied
immutableid, slug, domain, type, createdRejects the edit

Restriction Order (AC-02)

tombstoned > archived > deprecated > published > active > approved > review > draft

A merge of published and deprecated resolves to deprecated; merging tombstoned with anything yields tombstoned.

applyMergeStrategy Signature

applyMergeStrategy(doc, field, current, incoming, strategy, edit) → MergeResult { type, value } — pure function, no side effects on the Yjs doc until the caller commits.

Key Terms

  • PromptUnitDoc → the Yjs document type wrapping a unit’s mutable fields.
  • Manual resolution → a conflict that must be resolved by a human reviewer; blocks publish.
  • Recompute post-merge → fingerprint is derived, never merged.

Q&A

Q: Why is created marked immutable rather than last_write_wins? A: created is part of canonical identity; mutating it would silently change every downstream fingerprint and break content-addressing.

Q: Can a merge produce a unit that fails Zod validation? A: Yes — append_wins on steps can introduce an invalid composition. The validator runs after the merge and the unit lands in a tampered-adjacent state until corrected.

Examples

Editor A renames a step argument while Editor B appends a new step. contract triggers manual_resolution (blocking); steps resolves via append_wins; recompute_post_merge then issues a new blake3: digest.

neighbors on the map