Skip to content

Latest commit

 

History

History
70 lines (40 loc) · 7.04 KB

File metadata and controls

70 lines (40 loc) · 7.04 KB

Usage guide

This guide helps application authors choose a yin data model and synchronization flow. For installation and a first success, start with the README. Runnable programs are available in examples/.

Choose a CRDT

Use LWWRegister[T] when the entire value is one conflict boundary. It is a good fit for a setting, status, or other value where one concurrent assignment should deterministically win.

Use LWWMap[V] when independent string keys should merge independently. Each key is an LWW record with its own timestamp; Delete creates a tombstone so an older set cannot resurrect the key. This is usually the better starting point for collections and top-level JSON objects.

Both types order writes by Lamport counter and then by ReplicaID. This is deterministic conflict resolution, not wall-clock or user-intent ordering. Keep each logical replica's ReplicaID stable, unique, non-empty, and separate from temporary network identities. Core constructors do not reject an empty ID, but JSON delta and snapshot formats require non-empty update writers, so state written by an empty-ID replica cannot be encoded.

Model entities and conflict boundaries

An entity map gives each independently editable record a top-level key, such as card:<id>, column:<id>, or msg:<room>:<id>. Concurrent writes to different keys survive. Concurrent writes to the same key resolve by LWW and replace that key's whole value.

For LWWMap[json.RawMessage], only top-level fields are interpreted. Nested objects, arrays, scalars, and null are opaque payloads. Concurrent edits to two nested fields in the same payload do not merge. If those fields must merge independently, split them into top-level keys such as card:<id>:title and card:<id>:column.

Kanban-shaped data

A first kanban model can store columns and cards under separate entity keys. A card payload can contain title, column, and an application-defined sortable pos. Separate card keys allow concurrent edits to different cards to merge.

The trade-off is that two concurrent edits to one card compete as whole-card writes. Position values are also application policy, not an ordered-list CRDT. yin does not provide move rules, rebalance positions, or enforce board invariants. See the runnable kanban examples for independent edits and a same-entity conflict.

Chat-shaped data

Store room metadata under keys such as room:general and give each appended message an application-generated unique key such as msg:general:<id>. Concurrent appends then touch different records.

This shape does not provide message-ID allocation, log ordering, transport, reactions, membership, counters, or unread policy. Edits to one message remain whole-message LWW conflicts; richer behavior may require a different or finer-grained model. A runnable chat-shaped example is in the Go examples.

Choose an artifact

Do not substitute one encoding for another:

  • Projection: MarshalJSONObject emits visible, live top-level JSON. Use it for users and application code. It omits tombstones, timestamps, causal coverage, and local Lamport state; re-parsing it creates fresh CRDT state.
  • Snapshot: MarshalLWWMapSnapshotJSON preserves complete LWWMap[json.RawMessage] state for durable restore or whole-state fallback. Decode with UnmarshalLWWMapSnapshotJSON and the local replica identity.
  • Delta: ExtractDelta produces changes missing from a supplied VersionVector. Encode deltas with the typed codec matching the document and value type (JSONLWWMapDeltaCodec[V], JSONLWWRegisterDeltaCodec[T]) for incremental exchange.

These are transport-agnostic seams. The application owns framing, routing by delta kind and value type, stable replica identity mapping, retries, and storage policy. Encoded formats are experimental and pre-1.0; see the JSON format reference for their current exact contracts.

Incremental catch-up and fallback

For normal catch-up:

  1. The receiver sends its Version() as a cursor.
  2. The source calls ExtractDelta(cursor).
  3. If the result is non-nil, encode and route it to the matching decoder and document type.
  4. The receiver calls ApplyDelta; true means its state advanced.
  5. The receiver uses its resulting Version() as the cursor for a later request.

Yin reconstructs deltas from the CRDT state currently retained by the source; it does not require an operation log. An LWWRegister delta carries its retained winner, while an LWWMap delta carries the retained records missing from the cursor plus causal coverage. An application may still keep or batch encoded deltas as transport or storage policy, but that history is outside yin.

ExtractDelta may return nil when the cursor already covers the source. Do not pass that result to an encoder that expects a concrete delta. ApplyDelta returning false is a normal no-op signal for stale, duplicate, empty, incompatible, or otherwise non-advancing input; route compatible types yourself if you need stronger diagnostics.

A snapshot is not required merely because a peer is far behind: a live source can extract a delta from an old or empty cursor. Choose a snapshot when the application already has a durable snapshot artifact, wants an explicit whole-state bootstrap or fallback, or otherwise prefers whole-state transfer. Snapshot APIs currently support only LWWMap[json.RawMessage]. Decode the snapshot into a temporary map and Merge that state into the receiver, then resume delta catch-up using the receiver's resulting Version(). If the snapshot represented the unchanged source's current state, the next extraction should produce nil; if the snapshot was older, the source can extract the subsequent catch-up delta. See the runnable snapshot fallback example.

For durable restore, decode the latest snapshot using the continuing non-empty local ReplicaID, then apply only newer deltas from the restored document's version. Do not restore from a projection if tombstones and causal history must remain valid.

Concurrency and ownership

A yin document is not a synchronization primitive. Do not concurrently call methods on the same instance without application-level locking or single-owner serialization. Replicas may operate independently; synchronize by exchanging state or deltas rather than sharing one mutable instance across goroutines.

Generic LWWMap[V] values are treated as immutable after Set and after being returned by Get or Entries. For slices, maps, pointers, or other mutable values, copy them or otherwise prevent mutation outside a timestamped Set. LWWMap[json.RawMessage] is the exception with explicit byte-copy ownership: yin clones raw JSON at local, read, merge, extraction, and application boundaries.

Further reading