yin

package module
v0.1.1 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Jul 13, 2026 License: MIT Imports: 8 Imported by: 0

README

yin

CI Latest release

yin is an experimental, pre-1.0 Go module for dependency-free CRDT primitives and replicated data types. It provides causal versions, deterministic last-writer-wins state, top-level JSON object field merge, and transport-agnostic delta catch-up seams.

The module path is github.com/iamseth/yin. The repository is stdlib-only and exposes one root package, yin.

Install and try

yin requires Go 1.26. Add it to a Go module and run an included example:

go get github.com/iamseth/yin
go run github.com/iamseth/yin/examples/lww-basic

This is experimental, pre-1.0 software: APIs and encoded formats may change before a stable release.

Features today

  • ReplicaID, Lamport Timestamp, and VersionVector causal cursors.
  • Generic LWWRegister[T] for whole-value last-writer-wins state.
  • String-keyed LWWMap[V] with per-key conflict resolution and tombstone deletes.
  • Top-level JSON object parsing and projection through LWWMap[json.RawMessage].
  • Whole-state merge plus reconstructable, idempotent delta extraction and application.
  • Typed JSON delta codecs for registers and maps.
  • Durable JSON snapshots for LWWMap[json.RawMessage].
  • Deterministic and randomized convergence tests and runnable examples.

Concurrent writes are ordered by Lamport counter, then ReplicaID; this is not wall-clock ordering. Different map keys merge independently, while concurrent writes to one key replace that key's whole value. Nested JSON is opaque and does not merge recursively.

See the usage guide for choosing a CRDT, modeling kanban or chat data, conflict boundaries, encoding selection, catch-up and snapshot fallback, concurrency, and value ownership.

Quick LWWRegister example

package main

import (
	"fmt"

	"github.com/iamseth/yin"
)

func main() {
	newRegister := yin.NewLWWRegister[string]
	register := newRegister(yin.ReplicaID("replica-a"))
	register.Set("ready")

	fmt.Println(register.Value())
	fmt.Println(register.Timestamp().Counter())
}

The register is one conflict boundary. Use LWWMap when independent string keys should merge independently.

LWWMap and top-level JSON objects

LWWMap[V] gives each string key an independent conflict boundary. The top-level JSON helpers use LWWMap[json.RawMessage], so different root fields merge independently while each nested value remains opaque.

MarshalJSONObject is a user-visible projection, not durable persistence: it omits tombstones, timestamps, version vectors, and local Lamport state. Use snapshot APIs for durable CRDT state and deltas for incremental catch-up. The usage guide explains the distinction and fallback workflow; the JSON format reference specifies current encoded payloads.

VersionVector and delta catch-up

A receiver supplies its Version() cursor to ExtractDelta, then applies a non-nil result with ApplyDelta. A nil delta means there is currently nothing missing. ApplyDelta returns true only when the receiver advances; duplicate and stale replay is a normal false no-op.

For complete, executable flows, see:

Scope and caveats

  • yin supplies CRDT and serialization seams, not transport, peer discovery, session protocols, persistence loops, storage engines, or application business rules.
  • Keep logical ReplicaID values stable and distinct from temporary transport identities.
  • Generic mutable LWWMap[V] values require caller immutability; the json.RawMessage path clones bytes at API and synchronization boundaries.
  • A document instance requires application-level synchronization if accessed by multiple goroutines.
  • JSON projections, snapshots, and deltas have different purposes and are not interchangeable.
  • Recursive JSON, ordered-list semantics, and additional CRDTs are not implemented.

Read the usage guide for operational guidance, the JSON format reference for current payload contracts, DECISIONS.md for design rationale, and BACKLOG.md for planned work.

Releases and compatibility

Tagged releases follow Semantic Versioning. While yin remains on v0.x, minor releases may make breaking changes to public APIs or encoded formats, and compatibility is not guaranteed. Review release notes before upgrading. Patch releases are intended for backward-compatible fixes within their minor line.

Use the CI workflow to view current build status and the GitHub Releases page for published versions and release notes.

Project guidance

See CONTRIBUTING.md before proposing a change. Report suspected vulnerabilities according to SECURITY.md, not through a public issue.

License

MIT. See LICENSE.

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

Examples

Constants

This section is empty.

Variables

View Source
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

func MarshalDeltaJSON(delta Delta) ([]byte, error)

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

func UnmarshalLWWMapDeltaJSON[V any](data []byte) (Delta, error)

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

type DeltaCodec interface {
	EncodeDelta(Delta) ([]byte, error)
	DecodeDelta([]byte) (Delta, error)
}

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

func NewLWWMap[V any](replica ReplicaID) *LWWMap[V]

NewLWWMap constructs an empty string-keyed LWW map whose future local writes are issued by replica.

func ParseJSONObject

func ParseJSONObject(replica ReplicaID, data []byte) (*LWWMap[json.RawMessage], error)

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

func UnmarshalLWWMapSnapshotJSON(replica ReplicaID, data []byte) (*LWWMap[json.RawMessage], error)

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

func (m *LWWMap[V]) ApplyDelta(d Delta) (changed bool)

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

func (m *LWWMap[V]) Delete(key string)

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

func (m *LWWMap[V]) Entries() map[string]V

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

func (m *LWWMap[V]) Equal(other Document) bool

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

func (m *LWWMap[V]) Get(key string) (value V, ok bool)

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

func (m *LWWMap[V]) Merge(other Document) (changed bool)

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

func (m *LWWMap[V]) Set(key string, value V)

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.

func (ReplicaID) Compare

func (id ReplicaID) Compare(other ReplicaID) int

Compare orders ReplicaIDs using their documented deterministic total order. It returns -1 when id sorts before other, 1 when it sorts after other, and 0 when they are equal.

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

func NewTimestamp(counter uint64, replica ReplicaID) Timestamp

NewTimestamp constructs a Timestamp from an explicit Lamport counter and issuing ReplicaID.

func (Timestamp) Advance

func (ts Timestamp) Advance(seen Timestamp, replica ReplicaID) Timestamp

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

func (ts Timestamp) Compare(other Timestamp) int

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.

func (Timestamp) Counter

func (ts Timestamp) Counter() uint64

Counter returns the Lamport logical counter currently carried by ts.

func (Timestamp) ReplicaID

func (ts Timestamp) ReplicaID() ReplicaID

ReplicaID returns the replica that issued ts.

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

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

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.

Directories

Path Synopsis
examples
delta-catchup command
lww-basic command

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL