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 ¶
- type Authority
- type Clock
- type Counter
- type CounterState
- type Cursor
- type Mutation
- type Presence
- type PresenceSet
- type Record
- type Replica
- func (parseR *Replica) Delete(parseKey string)
- func (parseR *Replica) Export() ReplicaState
- func (parseR *Replica) Get(parseKey string) (string, bool)
- func (parseR *Replica) Merge(parseChanges []Record)
- func (parseR *Replica) Pending() []Mutation
- func (parseR *Replica) Set(parseKey, parseValue string) Record
- func (parseR *Replica) Snapshot() map[string]string
- type ReplicaState
- type Text
- type TextOp
- type TextState
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 (*Authority) Changes ¶
Changes returns every authoritative record (including tombstones), sorted by key — the full-state pull a replica merges to converge.
type Clock ¶
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.
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 RestoreCounter ¶
func RestoreCounter(parseState CounterState) *Counter
RestoreCounter rebuilds a counter from persisted state.
func (*Counter) Export ¶
func (parseC *Counter) Export() CounterState
Export captures the counter's state.
func (*Counter) Inc ¶
Inc adds by to the calling replica's positive tally (a local, conflict-free increment).
type CounterState ¶
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 ¶
End returns the higher of Anchor/Head — the selection's end regardless of drag direction.
func (Cursor) HasSelection ¶
HasSelection reports whether the cursor spans a range (Anchor != Head).
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 ¶
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 ¶
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) Export ¶
func (parseR *Replica) Export() ReplicaState
Export captures the replica's full state for durable persistence.
func (*Replica) Merge ¶
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 ¶
Pending returns a copy of the unsynced mutation log (what Push would send).
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 ¶
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 ¶
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 ¶
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 ¶
Export captures the text's state in a deterministic (convergent) op order, so two replicas with the same elements serialize identically.
func (*Text) Insert ¶
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) Merge ¶
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.
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.
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. |