Documentation
¶
Overview ¶
Package yin provides dependency-free CRDT primitives and replicated data types. Yin documents are not safe for concurrent use; callers must serialize access. Generic CRDT values should be treated as immutable unless a concrete API explicitly documents copy semantics.
Index ¶
- Variables
- func MarshalDeltaJSON(delta Delta) ([]byte, error)
- func MarshalJSONObject(fields *LWWMap[json.RawMessage]) ([]byte, error)
- func MarshalLWWMapSnapshotJSON(fields *LWWMap[json.RawMessage]) ([]byte, error)
- type Delta
- type DeltaCodec
- type Document
- type JSONLWWMapDeltaCodec
- type JSONLWWRegisterDeltaCodec
- type LWWMap
- func (m *LWWMap[V]) ApplyDelta(d Delta) (changed bool)
- func (m *LWWMap[V]) Delete(key string)
- func (m *LWWMap[V]) Entries() map[string]V
- func (m *LWWMap[V]) Equal(other Document) bool
- func (m *LWWMap[V]) ExtractDelta(since VersionVector) Delta
- func (m *LWWMap[V]) Get(key string) (value V, ok bool)
- func (m *LWWMap[V]) Merge(other Document) (changed bool)
- func (m *LWWMap[V]) Set(key string, value V)
- func (m *LWWMap[V]) Version() VersionVector
- type LWWRegister
- func (r *LWWRegister[T]) ApplyDelta(d Delta) (changed bool)
- func (r *LWWRegister[T]) Equal(other Document) bool
- func (r *LWWRegister[T]) ExtractDelta(since VersionVector) Delta
- func (r *LWWRegister[T]) Merge(other Document) (changed bool)
- func (r *LWWRegister[T]) Set(value T)
- func (r *LWWRegister[T]) Timestamp() Timestamp
- func (r *LWWRegister[T]) Value() T
- func (r *LWWRegister[T]) Version() VersionVector
- func (r *LWWRegister[T]) Writer() ReplicaID
- type ReplicaID
- type Timestamp
- type VersionVector
- func (v VersionVector) Clone() VersionVector
- func (v VersionVector) Compare(other VersionVector) VersionVectorComparison
- func (v VersionVector) Counters() map[ReplicaID]uint64
- func (v VersionVector) Equal(other VersionVector) bool
- func (v VersionVector) Get(replica ReplicaID) uint64
- func (v VersionVector) Increment(replica ReplicaID) VersionVector
- func (v VersionVector) Join(other VersionVector) VersionVector
- func (v VersionVector) Merge(other VersionVector) VersionVector
- func (v VersionVector) Since(other VersionVector) VersionVector
- type VersionVectorComparison
Examples ¶
- LWWMap (ChatLogShape)
- LWWMap (KanbanEntityMap)
- LWWMap (KanbanSameEntityConflict)
- LWWMap.Delete
- LWWMap.Merge
- LWWRegister.ApplyDelta
- LWWRegister.ExtractDelta
- LWWRegister.Merge
- LWWRegister.Set
- MarshalDeltaJSON (JsonObjectFlow)
- MarshalLWWMapSnapshotJSON (WholeStateFallback)
- VersionVector.Compare
- VersionVector.Since
Constants ¶
This section is empty.
Variables ¶
var ( // ErrUnsupportedDelta reports that a codec cannot encode the supplied Delta. ErrUnsupportedDelta = errors.New("yin: unsupported delta") // ErrMalformedDelta reports that encoded delta bytes are not valid for a codec. ErrMalformedDelta = errors.New("yin: malformed delta") )
Functions ¶
func MarshalDeltaJSON ¶
MarshalDeltaJSON encodes a supported Delta as transport-agnostic JSON bytes.
The codec currently supports LWWMap deltas. It returns a diagnostic error for nil deltas, unsupported delta kinds, or delta implementations that report a supported kind without carrying the matching reconstructable payload.
Example (JsonObjectFlow) ¶
package main
import (
"encoding/json"
"fmt"
"github.com/iamseth/yin"
)
func main() {
left, err := yin.ParseJSONObject(yin.ReplicaID("replica-a"), []byte(`{"title":"draft"}`))
if err != nil {
panic(err)
}
right := yin.NewLWWMap[json.RawMessage](yin.ReplicaID("replica-b"))
initialBytes, err := yin.MarshalDeltaJSON(left.ExtractDelta(right.Version()))
if err != nil {
panic(err)
}
initialDelta, err := yin.UnmarshalLWWMapDeltaJSON[json.RawMessage](initialBytes)
if err != nil {
panic(err)
}
right.ApplyDelta(initialDelta)
left.Set("status", json.RawMessage(`"review"`))
right.Set("author", json.RawMessage(`"Lin"`))
fromRightBytes, err := yin.MarshalDeltaJSON(right.ExtractDelta(left.Version()))
if err != nil {
panic(err)
}
fromRight, err := yin.UnmarshalLWWMapDeltaJSON[json.RawMessage](fromRightBytes)
if err != nil {
panic(err)
}
left.ApplyDelta(fromRight)
fromLeftBytes, err := yin.MarshalDeltaJSON(left.ExtractDelta(right.Version()))
if err != nil {
panic(err)
}
fromLeft, err := yin.UnmarshalLWWMapDeltaJSON[json.RawMessage](fromLeftBytes)
if err != nil {
panic(err)
}
right.ApplyDelta(fromLeft)
merged, err := yin.MarshalJSONObject(left)
if err != nil {
panic(err)
}
fmt.Println(string(merged))
fmt.Println(left.Equal(right))
}
Output: {"author":"Lin","status":"review","title":"draft"} true
func MarshalJSONObject ¶
func MarshalJSONObject(fields *LWWMap[json.RawMessage]) ([]byte, error)
MarshalJSONObject marshals the live entries of fields as one top-level JSON object. Deleted entries are omitted.
Field values are written from their json.RawMessage payloads without recursively interpreting or canonicalizing nested JSON.
func MarshalLWWMapSnapshotJSON ¶
func MarshalLWWMapSnapshotJSON(fields *LWWMap[json.RawMessage]) ([]byte, error)
MarshalLWWMapSnapshotJSON encodes the complete CRDT state of fields as a durable JSON snapshot.
The snapshot preserves live entries, tombstones, per-entry timestamps, and causal version coverage. It is distinct from MarshalJSONObject, which emits only the visible JSON object projection.
Example (WholeStateFallback) ¶
package main
import (
"encoding/json"
"fmt"
"github.com/iamseth/yin"
)
func main() {
sourceReplica := yin.ReplicaID("replica-a")
behindReplica := yin.ReplicaID("replica-b")
source := yin.NewLWWMap[json.RawMessage](sourceReplica)
source.Set("card:1", json.RawMessage(`{"title":"draft"}`))
source.Set("card:old", json.RawMessage(`{"title":"remove"}`))
source.Delete("card:old")
behind := yin.NewLWWMap[json.RawMessage](behindReplica)
snapshotBytes, err := yin.MarshalLWWMapSnapshotJSON(source)
if err != nil {
panic(err)
}
snapshot, err := yin.UnmarshalLWWMapSnapshotJSON(behindReplica, snapshotBytes)
if err != nil {
panic(err)
}
fmt.Println(behind.Merge(snapshot))
fmt.Println(source.ExtractDelta(behind.Version()) == nil) // already caught up
source.Set("card:2", json.RawMessage(`{"title":"later"}`))
catchUp := source.ExtractDelta(behind.Version())
if catchUp == nil {
panic("expected catch-up delta after source write")
}
deltaBytes, err := yin.MarshalDeltaJSON(catchUp)
if err != nil {
panic(err)
}
delta, err := yin.UnmarshalLWWMapDeltaJSON[json.RawMessage](deltaBytes)
if err != nil {
panic(err)
}
fmt.Println(behind.ApplyDelta(delta))
_, oldVisible := behind.Get("card:old")
visible, err := yin.MarshalJSONObject(behind)
if err != nil {
panic(err)
}
fmt.Println(oldVisible)
fmt.Println(string(visible))
fmt.Println(behind.Version().Equal(source.Version()))
}
Output: true true true false {"card:1":{"title":"draft"},"card:2":{"title":"later"}} true
Types ¶
type Delta ¶
type Delta interface {
// Kind returns a stable type tag for the concrete delta family.
//
// ApplyDelta implementations and future serialization codecs use this tag to
// route heterogeneous deltas without making the public Delta interface
// generic. Kind is not itself a wire format.
Kind() string
// Version returns the causal state fully represented by this delta.
//
// The returned VersionVector lets receivers and codecs reason about catch-up
// coverage while the concrete delta still owns its reconstructable payload.
Version() VersionVector
}
Delta is the non-generic public seam for document catch-up payloads.
A Delta implementation must carry all state needed to reconstruct and apply the represented document update for its concrete CRDT kind. The interface is intentionally small: Kind identifies the concrete delta family for routing or a future codec, and Version describes the causal state covered by the delta. Yin does not define a concrete wire encoding here; codecs should translate concrete delta implementations at this seam without changing Document.
func UnmarshalLWWMapDeltaJSON ¶
UnmarshalLWWMapDeltaJSON decodes bytes produced by MarshalDeltaJSON for an LWWMap delta whose values have JSON representation V.
The value type V must match the map value type that will receive the decoded delta via ApplyDelta. Unsupported kinds, structurally malformed payloads, and value payloads that cannot decode into V are returned as errors instead of being converted into no-op core deltas.
type DeltaCodec ¶
DeltaCodec serializes and deserializes concrete Delta values.
Codecs live at the non-generic Delta seam: EncodeDelta accepts any Delta and may reject deltas it does not support, while DecodeDelta returns a concrete Delta that can be applied to a compatible Document.
type Document ¶
type Document interface {
// Merge incorporates all state from a compatible whole-state document.
//
// Merge returns true only when the receiver's observable state or causal
// version changed. That changed/no-op signal is the same idempotent ingest
// signal exposed by ApplyDelta, and lets callers distinguish useful merges
// from stale replays without a second query.
//
// A false return can mean either incompatible type or stale, duplicate, or
// empty no-op input; the core signal does not distinguish those cases. Callers
// should ensure type compatibility at routing time.
Merge(other Document) (changed bool)
// ExtractDelta returns the reconstructable delta needed to catch a peer up
// from since to the receiver's current Version.
//
// Implementations may return nil when since already dominates or equals the
// receiver's causal state. The returned Delta must carry enough concrete
// state for ApplyDelta on a compatible document to reproduce the missing
// update without consulting the source document again.
ExtractDelta(since VersionVector) Delta
// ApplyDelta ingests a delta produced by ExtractDelta for a compatible
// document kind.
//
// ApplyDelta returns true only when the receiver's observable state or causal
// version changed. Stale, duplicate, or empty deltas should be idempotent
// no-ops that return false, giving replication consumers a single changed/no-op ingest signal.
//
// A false return can mean either incompatible type or stale, duplicate, or
// empty no-op input; the core signal does not distinguish those cases. Callers
// should ensure type compatibility at routing time.
ApplyDelta(d Delta) (changed bool)
// Equal reports whether two compatible whole-state documents are
// observationally equivalent.
//
// The convergence harness uses Merge plus Equal as the whole-state oracle:
// after every replica has merged the same state, Equal must report agreement.
Equal(other Document) bool
// Version returns the document's current causal version.
//
// Callers pass this VersionVector to ExtractDelta to request catch-up from a
// known cursor, and implementations should return a value that cannot be used
// to mutate the document's internal state.
Version() VersionVector
}
Document is the non-generic orchestration interface implemented by CRDT documents.
Concrete document types may be generic internally, but this interface stays non-generic so harnesses, routers, and a sync/replication layer can hold heterogeneous CRDTs. Merge and Equal provide the whole-state convergence oracle; ExtractDelta and ApplyDelta provide the delta catch-up path.
Implementations are not safe for concurrent use; callers must serialize all access to a document.
type JSONLWWMapDeltaCodec ¶
type JSONLWWMapDeltaCodec[V any] struct{}
JSONLWWMapDeltaCodec is a JSON codec for LWWMap[V] deltas.
func NewJSONLWWMapDeltaCodec ¶
func NewJSONLWWMapDeltaCodec[V any]() JSONLWWMapDeltaCodec[V]
NewJSONLWWMapDeltaCodec constructs a JSON codec for LWWMap[V] deltas.
func (JSONLWWMapDeltaCodec[V]) DecodeDelta ¶
func (JSONLWWMapDeltaCodec[V]) DecodeDelta(data []byte) (Delta, error)
DecodeDelta decodes an LWWMap[V] delta from JSON.
func (JSONLWWMapDeltaCodec[V]) EncodeDelta ¶
func (JSONLWWMapDeltaCodec[V]) EncodeDelta(d Delta) ([]byte, error)
EncodeDelta encodes a supported LWWMap[V] delta as JSON.
type JSONLWWRegisterDeltaCodec ¶
type JSONLWWRegisterDeltaCodec[T any] struct{}
JSONLWWRegisterDeltaCodec is a JSON codec for LWWRegister[T] deltas.
func NewJSONLWWRegisterDeltaCodec ¶
func NewJSONLWWRegisterDeltaCodec[T any]() JSONLWWRegisterDeltaCodec[T]
NewJSONLWWRegisterDeltaCodec constructs a JSON codec for LWWRegister[T] deltas.
func (JSONLWWRegisterDeltaCodec[T]) DecodeDelta ¶
func (JSONLWWRegisterDeltaCodec[T]) DecodeDelta(data []byte) (Delta, error)
DecodeDelta decodes an lwwRegisterDelta[T] from JSON.
func (JSONLWWRegisterDeltaCodec[T]) EncodeDelta ¶
func (JSONLWWRegisterDeltaCodec[T]) EncodeDelta(d Delta) ([]byte, error)
EncodeDelta encodes an lwwRegisterDelta[T] as JSON.
type LWWMap ¶
type LWWMap[V any] struct { // contains filtered or unexported fields }
LWWMap is a string-keyed last-writer-wins map for values of type V.
Each key stores its own update timestamp. Deletes are retained as internal tombstones so later merge and delta layers can distinguish an absent key from a removed key. Local writes are issued by replica and advance beyond the maximum counter already observed in the map's causal version.
Generic values are treated as immutable once handed to the map and once returned to callers. The JSON object path is the explicit exception: LWWMap[json.RawMessage] clones raw JSON bytes when values cross public and sync boundaries so callers cannot mutate CRDT state without a timestamped write.
Example (ChatLogShape) ¶
package main
import (
"encoding/json"
"fmt"
"github.com/iamseth/yin"
)
func main() {
phone := yin.NewLWWMap[json.RawMessage](yin.ReplicaID("replica-a"))
laptop := yin.NewLWWMap[json.RawMessage](yin.ReplicaID("replica-b"))
phone.Set("room:general", json.RawMessage(`{"title":"General"}`))
laptop.Merge(phone)
// Each appended message gets an application-chosen unique top-level key,
// so concurrent appends do not contend for the same LWW record.
phone.Set("msg:general:1", json.RawMessage(`{"author":"ada","body":"hello"}`))
laptop.Set("msg:general:2", json.RawMessage(`{"author":"ben","body":"hi"}`))
phone.Merge(laptop)
laptop.Merge(phone)
out, err := yin.MarshalJSONObject(phone)
if err != nil {
panic(err)
}
fmt.Println(string(out))
fmt.Println(phone.Equal(laptop))
}
Output: {"msg:general:1":{"author":"ada","body":"hello"},"msg:general:2":{"author":"ben","body":"hi"},"room:general":{"title":"General"}} true
Example (KanbanEntityMap) ¶
package main
import (
"encoding/json"
"fmt"
"github.com/iamseth/yin"
)
func main() {
desk := yin.NewLWWMap[json.RawMessage](yin.ReplicaID("replica-a"))
laptop := yin.NewLWWMap[json.RawMessage](yin.ReplicaID("replica-b"))
desk.Set("column:todo", json.RawMessage(`{"title":"Todo"}`))
desk.Set("card:1", json.RawMessage(`{"title":"Draft proposal","column":"todo","pos":"1000"}`))
desk.Set("card:2", json.RawMessage(`{"title":"Review notes","column":"todo","pos":"2000"}`))
laptop.Merge(desk)
desk.Set("card:1", json.RawMessage(`{"title":"Polish proposal","column":"todo","pos":"1000"}`))
laptop.Set("card:2", json.RawMessage(`{"title":"Review notes","column":"todo","pos":"3000"}`))
desk.Merge(laptop)
laptop.Merge(desk)
out, err := yin.MarshalJSONObject(desk)
if err != nil {
panic(err)
}
fmt.Println(string(out))
fmt.Println(desk.Equal(laptop))
}
Output: {"card:1":{"title":"Polish proposal","column":"todo","pos":"1000"},"card:2":{"title":"Review notes","column":"todo","pos":"3000"},"column:todo":{"title":"Todo"}} true
Example (KanbanSameEntityConflict) ¶
package main
import (
"encoding/json"
"fmt"
"github.com/iamseth/yin"
)
func main() {
desk := yin.NewLWWMap[json.RawMessage](yin.ReplicaID("replica-a"))
laptop := yin.NewLWWMap[json.RawMessage](yin.ReplicaID("replica-b"))
desk.Set("card:1", json.RawMessage(`{"title":"Draft proposal","column":"todo","pos":"1000"}`))
laptop.Merge(desk)
// These concurrent writes both replace the same top-level entity key.
// Their Lamport counters tie, so the ReplicaID tie-break makes replica-b win.
desk.Set("card:1", json.RawMessage(`{"title":"Polish proposal","column":"todo","pos":"1000"}`))
laptop.Set("card:1", json.RawMessage(`{"title":"Draft proposal","column":"doing","pos":"2000"}`))
desk.Merge(laptop)
laptop.Merge(desk)
out, err := yin.MarshalJSONObject(desk)
if err != nil {
panic(err)
}
fmt.Println(string(out))
fmt.Println(desk.Equal(laptop))
}
Output: {"card:1":{"title":"Draft proposal","column":"doing","pos":"2000"}} true
func NewLWWMap ¶
NewLWWMap constructs an empty string-keyed LWW map whose future local writes are issued by replica.
func ParseJSONObject ¶
ParseJSONObject parses data as a top-level JSON object and stores each field as an opaque json.RawMessage value in a new LWWMap.
Only the top-level object is interpreted. Nested objects, arrays, and scalar field values are retained as raw JSON payloads and are later ordered by the LWWMap's per-key timestamps.
func UnmarshalLWWMapSnapshotJSON ¶
UnmarshalLWWMapSnapshotJSON decodes a complete LWWMap[json.RawMessage] snapshot using replica as the restored map's local replica identity.
Stored records are loaded directly into CRDT state; decoding does not replay them as new local writes.
func (*LWWMap[V]) ApplyDelta ¶
ApplyDelta incorporates compatible LWWMap delta records and causal coverage. Nil, incompatible, duplicate, and fully stale deltas are false no-ops. Causal version advancement includes safe version-only coverage carried by the delta for same-key writes that lost to retained records.
func (*LWWMap[V]) Delete ¶
Delete applies a local delete for key and retains an internal tombstone.
Example ¶
package main
import (
"fmt"
"github.com/iamseth/yin"
)
func main() {
source := yin.NewLWWMap[string](yin.ReplicaID("replica-a"))
lagging := yin.NewLWWMap[string](yin.ReplicaID("replica-b"))
source.Set("title", "draft")
lagging.Merge(source)
source.Delete("title")
fmt.Println(source.Merge(lagging))
fmt.Println(lagging.Merge(source))
fmt.Println(lagging.Merge(source))
_, ok := lagging.Get("title")
fmt.Println(ok)
}
Output: false true false false
func (*LWWMap[V]) Entries ¶
Entries returns a copy of the map's live entries. Tombstones are omitted. For V == json.RawMessage, each returned value is a copy of the stored raw bytes.
func (*LWWMap[V]) Equal ¶
Equal reports whether other is a compatible LWW map with the same entries, tombstones, per-key timestamps, and causal version coverage.
func (*LWWMap[V]) ExtractDelta ¶
func (m *LWWMap[V]) ExtractDelta(since VersionVector) Delta
ExtractDelta returns the map records whose per-key timestamps are not already dominated by since, plus source causal coverage missing from since. A nil delta means since is caught up for the map's full causal version; otherwise the delta may carry version-only coverage for same-key writes that lost to retained records.
func (*LWWMap[V]) Get ¶
Get returns the live value stored at key. Deleted or missing keys report ok false and return V's zero value. For V == json.RawMessage, Get returns a copy of the stored raw bytes.
func (*LWWMap[V]) Merge ¶
Merge incorporates entries and causal coverage from a compatible LWW map.
Example ¶
package main
import (
"fmt"
"github.com/iamseth/yin"
)
func main() {
left := yin.NewLWWMap[string](yin.ReplicaID("replica-a"))
right := yin.NewLWWMap[string](yin.ReplicaID("replica-b"))
left.Set("title", "draft")
right.Set("status", "review")
fmt.Println(left.Merge(right))
fmt.Println(right.Merge(left))
title, _ := left.Get("title")
status, _ := left.Get("status")
fmt.Println(title, status)
fmt.Println(left.Equal(right))
}
Output: true true draft review true
func (*LWWMap[V]) Set ¶
Set applies a local write that stores value at key.
For most V types, value is stored as-is and must be treated as immutable by the caller after Set returns. For V == json.RawMessage, Set clones the raw bytes before storing them.
func (*LWWMap[V]) Version ¶
func (m *LWWMap[V]) Version() VersionVector
Version returns a copy of the map's observed causal version, including tombstone updates.
type LWWRegister ¶
type LWWRegister[T any] struct { // contains filtered or unexported fields }
LWWRegister is a last-writer-wins register for values of type T.
Local writes use Lamport timestamps issued by replica. Conflicting writes are ordered by Timestamp.Compare (counter, then issuing ReplicaID), making merge and delta apply deterministic without consulting wall-clock time.
Generic values are stored and returned as-is. Callers using mutable value types such as slices, maps, or pointers must treat them as immutable once they cross the register API boundary.
func NewLWWRegister ¶
func NewLWWRegister[T any](replica ReplicaID) *LWWRegister[T]
NewLWWRegister constructs an empty register whose future local writes are issued by replica.
func (*LWWRegister[T]) ApplyDelta ¶
func (r *LWWRegister[T]) ApplyDelta(d Delta) (changed bool)
ApplyDelta incorporates a compatible LWW register delta.
Example ¶
package main
import (
"fmt"
"github.com/iamseth/yin"
)
func main() {
source := yin.NewLWWRegister[string](yin.ReplicaID("replica-a"))
target := yin.NewLWWRegister[string](yin.ReplicaID("replica-b"))
source.Set("v1")
delta := source.ExtractDelta(target.Version())
first := target.ApplyDelta(delta)
duplicate := target.ApplyDelta(delta)
fmt.Println(first)
fmt.Println(duplicate)
fmt.Println(target.Value())
fmt.Println(target.Version().Equal(source.Version()))
}
Output: true false v1 true
func (*LWWRegister[T]) Equal ¶
func (r *LWWRegister[T]) Equal(other Document) bool
Equal reports whether other is a compatible LWW register with the same value and timestamp. Values are compared with reflect.DeepEqual so the concrete register can remain LWWRegister[T any]. Under reflect.DeepEqual NaN semantics, registers holding NaN with identical timestamps report Equal == false.
func (*LWWRegister[T]) ExtractDelta ¶
func (r *LWWRegister[T]) ExtractDelta(since VersionVector) Delta
ExtractDelta returns the current winning write when since does not already dominate it. A nil delta means since is already caught up for this register's retained LWW state.
Example ¶
package main
import (
"fmt"
"github.com/iamseth/yin"
)
func main() {
sourceReplica := yin.ReplicaID("replica-a")
source := yin.NewLWWRegister[string](sourceReplica)
target := yin.NewLWWRegister[string](yin.ReplicaID("replica-b"))
source.Set("v1")
delta := source.ExtractDelta(target.Version())
fmt.Println(delta.Kind())
fmt.Println(delta.Version().Get(sourceReplica))
fmt.Println(source.ExtractDelta(source.Version()) == nil)
}
Output: lww-register 1 true
func (*LWWRegister[T]) Merge ¶
func (r *LWWRegister[T]) Merge(other Document) (changed bool)
Merge incorporates the winning write from a compatible LWW register.
Example ¶
package main
import (
"fmt"
"github.com/iamseth/yin"
)
func main() {
left := yin.NewLWWRegister[string](yin.ReplicaID("replica-a"))
right := yin.NewLWWRegister[string](yin.ReplicaID("replica-b"))
left.Set("from a")
right.Set("from b")
changed := left.Merge(right)
duplicate := left.Merge(right)
fmt.Println(changed)
fmt.Println(duplicate)
fmt.Println(left.Value())
fmt.Println(left.Equal(right))
}
Output: true false from b true
func (*LWWRegister[T]) Set ¶
func (r *LWWRegister[T]) Set(value T)
Set applies a local write to the register.
The new timestamp's counter is max(seen)+1. Because an LWW register retains only the current winning write, and losing writes never have a greater Lamport counter than that winner, the current timestamp is the maximum seen counter needed for the next local write. Value is stored as-is and must be treated as immutable by the caller after Set returns.
Example ¶
package main
import (
"fmt"
"github.com/iamseth/yin"
)
func main() {
register := yin.NewLWWRegister[string](yin.ReplicaID("replica-a"))
register.Set("ready")
fmt.Println(register.Value())
fmt.Println(register.Writer())
fmt.Println(register.Timestamp().Counter())
}
Output: ready replica-a 1
func (*LWWRegister[T]) Timestamp ¶
func (r *LWWRegister[T]) Timestamp() Timestamp
Timestamp returns the timestamp of the current winning write.
func (*LWWRegister[T]) Value ¶
func (r *LWWRegister[T]) Value() T
Value returns the register's current value. Mutable values must be treated as read-only by callers.
func (*LWWRegister[T]) Version ¶
func (r *LWWRegister[T]) Version() VersionVector
Version returns the causal version of the current winning write.
func (*LWWRegister[T]) Writer ¶
func (r *LWWRegister[T]) Writer() ReplicaID
Writer returns the ReplicaID that issued the current winning write.
type ReplicaID ¶
type ReplicaID string
ReplicaID is an opaque, comparable identity for one logical CRDT replica.
Consumers map their own durable identity (device, user, process, or transport peer) onto this value; yin neither derives nor generates ReplicaIDs.
ReplicaID has a deterministic total order: lexicographic byte order of the underlying string. That order is only for CRDT tie-breaking and does not imply creation time, trust, network topology, or transport identity.
type Timestamp ¶
type Timestamp struct {
// contains filtered or unexported fields
}
Timestamp identifies and orders CRDT updates.
The current representation is a Lamport logical counter plus the ReplicaID that issued the timestamp. The fields are intentionally unexported so a future HLC implementation can replace or extend the representation while preserving the comparison and advance contracts. Timestamp never consults wall-clock time, keeping generated histories deterministic and reproducible.
func NewTimestamp ¶
NewTimestamp constructs a Timestamp from an explicit Lamport counter and issuing ReplicaID.
func (Timestamp) Advance ¶
Advance returns the next local timestamp for replica after observing seen. The returned Lamport counter is max(ts.counter, seen.counter)+1; no wall-clock input is used. Advance panics if the next counter would overflow; silently wrapping would violate Lamport monotonicity.
func (Timestamp) Compare ¶
Compare orders timestamps by Lamport counter, then by ReplicaID as a deterministic tie-breaker. It returns -1 when ts sorts before other, 1 when it sorts after other, and 0 when both components are equal.
type VersionVector ¶
type VersionVector struct {
// contains filtered or unexported fields
}
VersionVector records the latest counter known for each logical replica.
Missing replicas are equivalent to counter zero. VersionVector is a value type: methods that advance or combine vectors return a new vector and leave their receivers unchanged, and methods that expose counters return copies.
func NewVersionVector ¶
func NewVersionVector(counters map[ReplicaID]uint64) VersionVector
NewVersionVector constructs a vector from counters, copying the input map. Zero counters are omitted because they are equivalent to absent replicas.
func (VersionVector) Clone ¶
func (v VersionVector) Clone() VersionVector
Clone returns an independent copy of v.
func (VersionVector) Compare ¶
func (v VersionVector) Compare(other VersionVector) VersionVectorComparison
Compare returns the causal relationship between v and other.
Example ¶
package main
import (
"fmt"
"github.com/iamseth/yin"
)
func main() {
replicaA := yin.ReplicaID("replica-a")
replicaB := yin.ReplicaID("replica-b")
server := yin.NewVersionVector(map[yin.ReplicaID]uint64{
replicaA: 2,
replicaB: 1,
})
client := yin.NewVersionVector(map[yin.ReplicaID]uint64{
replicaA: 1,
})
peer := yin.NewVersionVector(map[yin.ReplicaID]uint64{
replicaA: 1,
replicaB: 2,
})
fmt.Println(server.Compare(client))
fmt.Println(server.Compare(peer))
}
Output: dominates concurrent
func (VersionVector) Counters ¶
func (v VersionVector) Counters() map[ReplicaID]uint64
Counters returns a copy of v's non-zero per-replica counters.
func (VersionVector) Equal ¶
func (v VersionVector) Equal(other VersionVector) bool
Equal reports whether v and other carry the same counters for every replica, treating absent replicas as counter zero.
func (VersionVector) Get ¶
func (v VersionVector) Get(replica ReplicaID) uint64
Get returns the counter known for replica. Missing replicas read as zero.
func (VersionVector) Increment ¶
func (v VersionVector) Increment(replica ReplicaID) VersionVector
Increment returns a copy of v with replica's counter advanced by one.
Increment panics if the replica counter is already at uint64's maximum value; silently wrapping would violate causal monotonicity.
func (VersionVector) Join ¶
func (v VersionVector) Join(other VersionVector) VersionVector
Join returns the pointwise maximum of v and other.
func (VersionVector) Merge ¶
func (v VersionVector) Merge(other VersionVector) VersionVector
Merge is an alias for Join, returning the pointwise maximum of v and other.
func (VersionVector) Since ¶
func (v VersionVector) Since(other VersionVector) VersionVector
Since returns the per-replica counters from v that are strictly greater than those in other. The returned counters are v's current counters, not numeric differences, so they can act as a causal cursor for catch-up delta extraction.
Example ¶
package main
import (
"fmt"
"github.com/iamseth/yin"
)
func main() {
replicaA := yin.ReplicaID("replica-a")
replicaB := yin.ReplicaID("replica-b")
current := yin.NewVersionVector(map[yin.ReplicaID]uint64{
replicaA: 3,
replicaB: 1,
})
cursor := yin.NewVersionVector(map[yin.ReplicaID]uint64{
replicaA: 1,
})
missing := current.Since(cursor)
fmt.Println(missing.Get(replicaA))
fmt.Println(missing.Get(replicaB))
}
Output: 3 1
type VersionVectorComparison ¶
type VersionVectorComparison int
VersionVectorComparison describes the causal relationship between two VersionVectors.
const ( // VersionVectorEqual means both vectors contain the same counters for every // replica, treating missing replicas as counter zero. VersionVectorEqual VersionVectorComparison = iota // VersionVectorDominates means the left vector is greater than or equal to // the right vector for every replica and strictly greater for at least one. VersionVectorDominates // VersionVectorDominatedBy means the left vector is less than or equal to the // right vector for every replica and strictly less for at least one. VersionVectorDominatedBy // VersionVectorConcurrent means neither vector dominates the other. VersionVectorConcurrent )
func (VersionVectorComparison) String ¶
func (c VersionVectorComparison) String() string
String returns a stable name for the comparison result.
Source Files
¶
Directories
¶
| Path | Synopsis |
|---|---|
|
examples
|
|
|
conflict-resolution
command
|
|
|
delta-catchup
command
|
|
|
lww-basic
command
|