localfirst

package
v4.2.0 Latest Latest
Warning

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

Go to latest
Published: Jul 5, 2026 License: MIT Imports: 3 Imported by: 0

Documentation

Overview

Package localfirst is GoWebComponents' built-in local-first sync engine (FC1). It gives a client an optimistic, offline-capable local store that converges with a server- authoritative store the moment connectivity returns — the model behind Zero / Electric / TanStack DB, in pure Go on both sides.

The core is a last-write-wins register per key (an LWW-Register CRDT). Every write carries a logical Clock; conflicts are resolved by the SAME deterministic rule on every replica and on the server, so all participants converge to one state regardless of the order in which offline edits arrive. A Replica applies writes locally at once (optimistic UI), keeps unsynced writes in a durable pending log (offline), and converges via Merge; an Authority is the server-side store. Mutation and Record are JSON-serializable, so they ride the //gwc:server transport (serverfn) unchanged.

This is the convergence engine. The query-driven "shapes" (`//gwc:sync`) and the db/sqlite + kvstate persistence adapters layer on top of it; kvstate supplies the single-replica conflict resolver, localfirst supplies the multi-replica protocol.

Index

Constants

This section is empty.

Variables

This section is empty.

Functions

This section is empty.

Types

type Authority

type Authority struct {
	// contains filtered or unexported fields
}

Authority is the server-authoritative store: it resolves incoming client mutations under the same LWW rule and serves the converged state back. Safe for concurrent use.

func NewAuthority

func NewAuthority() *Authority

NewAuthority creates an empty server-side store.

func (*Authority) Changes

func (parseA *Authority) Changes() []Record

Changes returns every authoritative record (including tombstones), sorted by key — the full-state pull a replica merges to converge.

func (*Authority) Receive

func (parseA *Authority) Receive(parseMutations []Mutation) []Record

Receive applies a batch of client mutations under LWW and returns the resulting authoritative records for the affected keys (their post-merge winning state), sorted by key, so the client can pull and converge.

func (*Authority) Snapshot

func (parseA *Authority) Snapshot() map[string]string

Snapshot returns the authority's live key/value state (tombstones excluded).

type Clock

type Clock struct {
	Counter   uint64 `json:"counter"`
	ReplicaID string `json:"replica"`
}

Clock is a logical, Lamport-style timestamp for last-write-wins resolution: the higher Counter wins, and ReplicaID breaks ties so every replica resolves any conflict identically — the property that makes the store convergent rather than merely eventually-something.

func (Clock) After

func (parseC Clock) After(parseOther Clock) bool

After reports whether c should win over o under last-write-wins.

type Counter

type Counter struct {
	// contains filtered or unexported fields
}

Counter is a positive-negative counter CRDT (PN-Counter). Unlike the last-write-wins Register (where two replicas that both edit the same key keep only one write), every replica's increments and decrements SURVIVE a merge: each replica tracks its own positive and negative tallies, Value is their global sum, and Merge takes the per-replica maximum — which is commutative, associative, and idempotent, so all replicas converge to the same count regardless of the order merges arrive. This is the op-based answer to "concurrent same-field edits must not silently last-write-wins" for the counter case (likes, votes, inventory, …). Safe for concurrent use is the caller's responsibility (guard with a mutex if shared across goroutines); the type is value-semantic per replica.

func NewCounter

func NewCounter() *Counter

NewCounter creates a zero counter.

func RestoreCounter

func RestoreCounter(parseState CounterState) *Counter

RestoreCounter rebuilds a counter from persisted state.

func (*Counter) Dec

func (parseC *Counter) Dec(parseReplica string, parseBy uint64)

Dec adds by to the calling replica's negative tally.

func (*Counter) Export

func (parseC *Counter) Export() CounterState

Export captures the counter's state.

func (*Counter) Inc

func (parseC *Counter) Inc(parseReplica string, parseBy uint64)

Inc adds by to the calling replica's positive tally (a local, conflict-free increment).

func (*Counter) Merge

func (parseC *Counter) Merge(parseOther *Counter)

Merge converges this counter with another by taking the per-replica maximum of each tally. It is commutative/associative/idempotent, so merging in any order — even merging the same peer twice — yields the same value, and no replica's increments are ever lost.

func (*Counter) Value

func (parseC *Counter) Value() int64

Value returns the current global count: the sum of every replica's increments minus the sum of every replica's decrements.

type CounterState

type CounterState struct {
	Pos map[string]uint64 `json:"pos"`
	Neg map[string]uint64 `json:"neg"`
}

CounterState is the JSON-serializable state of a Counter, for persistence/transport.

type Cursor

type Cursor struct {
	// ClientID identifies the peer the cursor belongs to.
	ClientID string `json:"client"`
	// Anchor is the primary caret position (an app-defined offset/index).
	Anchor int `json:"anchor"`
	// Head is the moving end of a selection; equal to Anchor when there is no selection.
	Head int `json:"head"`
	// Label is an optional display name shown next to the cursor.
	Label string `json:"label,omitempty"`
}

Cursor is a typed collaboration cursor/selection — where a peer is looking and what (if anything) they have selected, for live multiplayer awareness. It is presence-shaped (ephemeral, last-write, never conflict-merged): "where the cursor is right now" has no history. Carry it as the State payload of a Presence (JSON-encode it) so cursors ride the existing PresenceSet without a new transport.

func (Cursor) End

func (parseC Cursor) End() int

End returns the higher of Anchor/Head — the selection's end regardless of drag direction.

func (Cursor) HasSelection

func (parseC Cursor) HasSelection() bool

HasSelection reports whether the cursor spans a range (Anchor != Head).

func (Cursor) Start

func (parseC Cursor) Start() int

Start returns the lower of Anchor/Head — the selection's start regardless of drag direction.

type Mutation

type Mutation struct {
	Record Record `json:"record"`
}

Mutation is one local change awaiting sync — the durable offline-queue entry.

type Presence

type Presence struct {
	// ClientID identifies the peer (one per browser tab/session).
	ClientID string `json:"client"`
	// State is an opaque, app-defined JSON payload (e.g. a cursor and a display name).
	State string `json:"state"`
}

Presence is one client's ephemeral awareness state — cursor position, name, colour, "is typing", etc. It is deliberately NOT a synced Record: "who is here right now" has no history and must never be conflict-merged or persisted, so presence is last-write plus heartbeat-expiry. With the converging document store (Replica/Authority), this is the second half of real-time collaboration (FC6), which the sync engine gives nearly for free.

type PresenceSet

type PresenceSet struct {
	// contains filtered or unexported fields
}

PresenceSet tracks the live peers in a collaboration session, expiring any that miss heartbeats. It owns no wall clock: the app advances time by calling Tick on its heartbeat interval, which keeps the set deterministic and unit-testable. Safe for concurrent use.

func NewPresenceSet

func NewPresenceSet(parseTTL uint64) *PresenceSet

NewPresenceSet creates a presence set whose peers expire after ttl heartbeat ticks without a refresh. A ttl of 0 means a peer expires on the next Tick unless refreshed.

func (*PresenceSet) Count

func (parseP *PresenceSet) Count() int

Count returns the number of live peers.

func (*PresenceSet) Live

func (parseP *PresenceSet) Live() []Presence

Live returns the currently-present peers, sorted by client id for stable rendering.

func (*PresenceSet) Remove

func (parseP *PresenceSet) Remove(parseClientID string)

Remove drops a peer immediately (an explicit leave / tab close).

func (*PresenceSet) Tick

func (parseP *PresenceSet) Tick()

Tick advances the heartbeat clock by one and removes peers that have not been refreshed within ttl ticks — the expiry that makes a crashed or closed tab disappear from the session without an explicit leave.

func (*PresenceSet) Update

func (parseP *PresenceSet) Update(parsePresence Presence)

Update records or refreshes a peer's presence, marking it seen at the current tick.

type Record

type Record struct {
	Key     string `json:"key"`
	Value   string `json:"value"`
	Clock   Clock  `json:"clock"`
	Deleted bool   `json:"deleted"`
}

Record is one synced key/value with the clock that stamped it and a tombstone flag for deletes (deletes must propagate, so they are records, not absences).

func Sync

func Sync(parseReplica *Replica, parseAuthority *Authority) []Record

Sync performs one round-trip of the protocol: it pushes the replica's pending mutations to the authority and merges the full authoritative state back, converging the replica. Calling it again with an empty pending log is a pure pull (used to fan out another replica's accepted writes). Returns the records merged.

type Replica

type Replica struct {
	// contains filtered or unexported fields
}

Replica is a client-side local-first store: optimistic local writes, a durable pending log of unsynced writes (survives offline), and convergent Merge of authoritative records. Safe for concurrent use.

func NewReplica

func NewReplica(parseID string) *Replica

NewReplica creates an empty replica with a stable, globally-unique id (the id breaks write-conflict ties, so distinct replicas must use distinct ids).

func RestoreReplica

func RestoreReplica(parseState ReplicaState) *Replica

RestoreReplica rebuilds a replica from persisted state — typically read back from storage on page load — so its records and, crucially, its unsynced pending writes are exactly as they were before the reload.

func (*Replica) Delete

func (parseR *Replica) Delete(parseKey string)

Delete tombstones key optimistically and queues the delete for sync.

func (*Replica) Export

func (parseR *Replica) Export() ReplicaState

Export captures the replica's full state for durable persistence.

func (*Replica) Get

func (parseR *Replica) Get(parseKey string) (string, bool)

Get returns the live value for key (absent for unknown or tombstoned keys).

func (*Replica) Merge

func (parseR *Replica) Merge(parseChanges []Record)

Merge converges the replica toward a batch of authoritative records: each record is applied iff its clock wins under LWW, the local counter advances past it, and any pending mutation the authority has now acknowledged (its clock is equal-or-newer) is dropped.

func (*Replica) Pending

func (parseR *Replica) Pending() []Mutation

Pending returns a copy of the unsynced mutation log (what Push would send).

func (*Replica) Set

func (parseR *Replica) Set(parseKey, parseValue string) Record

Set writes key=value optimistically (visible immediately) and queues it for sync.

func (*Replica) Snapshot

func (parseR *Replica) Snapshot() map[string]string

Snapshot returns the replica's live key/value state (tombstones excluded) — used to assert convergence.

type ReplicaState

type ReplicaState struct {
	ID      string            `json:"id"`
	Counter uint64            `json:"counter"`
	Records map[string]Record `json:"records"`
	Pending []Mutation        `json:"pending"`
}

ReplicaState is the full, JSON-serializable state of a replica — its records, its unsynced pending log, and its logical counter. Persist it (localStorage / IndexedDB / db/sqlite) so a replica survives a page reload with its offline writes intact, then Restore it: the durable offline queue that makes "edit offline, close the tab, reopen, reconnect, converge" actually work.

type Text

type Text struct {
	// contains filtered or unexported fields
}

Text is an op-based collaborative-text CRDT — a Replicated Growable Array (RGA). Where the last-write-wins Register keeps only one of two concurrent edits to a string field, Text keeps BOTH: every inserted character is an immutable element with a globally-unique id, positioned relative to the element it was typed after, and deletions are tombstones. All replicas converge to the same string regardless of the order edits and merges arrive, and no concurrent insertion is ever silently dropped. This is the answer to the audit's collaborative-text gap that the PN-Counter (integers only) could not cover.

Text is value-semantic per replica; guard a Text shared across goroutines with a mutex. Each replica must construct its Text with a distinct, stable replica id (see NewText).

func NewText

func NewText(parseReplica string) *Text

NewText creates an empty collaborative text owned by the given replica id. The id must be unique per participant (a user/session/device id) and stable for that participant, because it breaks ties between concurrent insertions at the same position.

func RestoreText

func RestoreText(parseState TextState, parseReplica string) *Text

RestoreText rebuilds a Text from persisted state under the given replica id. The replica id is supplied by the caller (not read from the state) so a participant can adopt a shared document under its own identity; the local sequence clock is advanced past every restored id.

func (*Text) Delete

func (parseT *Text) Delete(parseIndex int, parseCount int)

Delete tombstones count visible characters starting at visible position index. Out-of-range requests are clamped; deleting an already-deleted element is a no-op.

func (*Text) Export

func (parseT *Text) Export() TextState

Export captures the text's state in a deterministic (convergent) op order, so two replicas with the same elements serialize identically.

func (*Text) Insert

func (parseT *Text) Insert(parseIndex int, parseString string)

Insert places s immediately before visible position index (0 = start, Len = append). Each character becomes a new element chained after the previous, so a multi-character insert stays contiguous and converges as a unit.

func (*Text) Len

func (parseT *Text) Len() int

Len returns the number of visible (non-deleted) characters.

func (*Text) Merge

func (parseT *Text) Merge(parseOther *Text)

Merge converges this text with another by unioning their element sets: an element present in either side is kept, and a tombstone on either side wins (deleted is monotonic). The local sequence clock is advanced past every merged id so future local inserts mint fresh, ordered ids. Merge is commutative, associative, and idempotent.

func (*Text) Value

func (parseT *Text) Value() string

Value returns the current text: every non-tombstoned element in convergent order.

type TextOp

type TextOp struct {
	Replica      string `json:"r"`
	Seq          uint64 `json:"s"`
	AfterReplica string `json:"ar,omitempty"`
	AfterSeq     uint64 `json:"as,omitempty"`
	Char         string `json:"c,omitempty"`
	Deleted      bool   `json:"d,omitempty"`
}

TextOp is the JSON-serializable form of one character element, for persistence/transport.

type TextState

type TextState struct {
	Replica string   `json:"replica"`
	Ops     []TextOp `json:"ops"`
}

TextState is the full op log of a Text — the set of character elements — for persistence and transport. Being op-based, the state IS the operation set.

Directories

Path Synopsis
Package facepile renders collaboration presence as a GoWebComponents component (FC6) — the "who's here right now" surface over a localfirst.PresenceSet.
Package facepile renders collaboration presence as a GoWebComponents component (FC6) — the "who's here right now" surface over a localfirst.PresenceSet.

Jump to

Keyboard shortcuts

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