kvstate

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: 10 Imported by: 0

Documentation

Overview

Package kvstate opts GWC state into transparent, durable key/value storage backed by the client-side SQLite driver (db/sqlite).

It mirrors ui.UsePersistedState (which uses localStorage) but routes through SQLite/IndexedDB, and every configurable axis is also a public interface a user can implement to build their own strategy:

  • PersistenceBackend — where values live (default: a shared SQLite engine).
  • WriteStrategy — when a dirty value becomes durable (Immediate, Debounced, OnUnload).
  • Codec — how values are encoded (JSONCodec, CBORCodec).
  • ConflictResolver — how concurrent/cross-tab writes are reconciled (LastWriteWins, Versioned).

Two ergonomic entry points share one engine:

  • UsePersistedState[T] — a component-bound hook (async hydrate, write-through).
  • BindAtom[T] — a global-atom adapter.

On native/SSR builds the underlying ui/interop stubs degrade gracefully; the SQLite engine itself still works (file-backed) so the durable logic is testable.

Index

Constants

This section is empty.

Variables

This section is empty.

Functions

func Import

func Import(parseCtx context.Context, parseBackend PersistenceBackend, parseRecords []Record, parseResolver ConflictResolver) error

Import writes records into a backend (ingress), resolving conflicts against any existing record with parseResolver. A nil resolver defaults to LastWriteWins. Records the resolver rejects (e.g. stale Versioned writes) are skipped, so an import never clobbers newer local state.

func RegisterBackend

func RegisterBackend(parseName string, parseBackend PersistenceBackend)

RegisterBackend registers a PersistenceBackend under name.

func RegisterCodec

func RegisterCodec(parseName string, parseCodec Codec)

RegisterCodec registers a Codec under name.

func RegisterResolver

func RegisterResolver(parseName string, parseResolver ConflictResolver)

RegisterResolver registers a ConflictResolver under name.

func RegisterStrategy

func RegisterStrategy(parseName string, parseStrategy WriteStrategy)

RegisterStrategy registers a WriteStrategy under name.

Types

type BoundAtom

type BoundAtom[T any] struct {
	// contains filtered or unexported fields
}

BoundAtom is a global state.Atom whose value is durably persisted. Its Set/Update write through to the backend; reads come from the atom.

Atoms expose no global out-of-component subscription, so — like ui.UsePersistedState — persistence is captured through this returned handle.

func BindAtom

func BindAtom[T any](parseCtx context.Context, parseAtom state.Atom[T], parseKey string, parseOptions ...Options) BoundAtom[T]

BindAtom binds a global atom to durable storage under parseKey. It loads the stored value asynchronously and applies it to the atom, then returns a write-through handle. Cross-tab writes are reflected back into the atom.

func (BoundAtom[T]) Err

func (parseB BoundAtom[T]) Err() error

Err returns the last persistence error, or nil.

func (BoundAtom[T]) Get

func (parseB BoundAtom[T]) Get() T

Get returns the current atom value.

func (BoundAtom[T]) Loading

func (parseB BoundAtom[T]) Loading() bool

Loading reports whether the initial hydrate is still pending.

func (BoundAtom[T]) Set

func (parseB BoundAtom[T]) Set(parseValue T)

Set updates the atom and persists the value.

func (BoundAtom[T]) Update

func (parseB BoundAtom[T]) Update(parseFn func(T) T)

Update updates the atom from its previous value and persists the result.

type CBORCodec

type CBORCodec struct{}

CBORCodec encodes values as CBOR (compact binary). Uses the cbor dependency already vendored by the module.

func (CBORCodec) Decode

func (CBORCodec) Decode(parseData []byte, parseTarget any) error

func (CBORCodec) Encode

func (CBORCodec) Encode(parseValue any) ([]byte, error)

type Codec

type Codec interface {
	Encode(parseValue any) ([]byte, error)
	Decode(parseData []byte, parseTarget any) error
}

Codec encodes and decodes persisted values. Implement it to control the wire format (e.g. a compact or encrypted representation).

func CodecByName

func CodecByName(parseName string) (Codec, bool)

CodecByName returns a registered Codec.

type ConflictResolver

type ConflictResolver interface {
	Resolve(parseLocal, parseIncoming Record) Record
}

ConflictResolver decides the winner when a local record meets an incoming one (e.g. a cross-tab write). Implement it for custom merge semantics.

func ResolverByName

func ResolverByName(parseName string) (ConflictResolver, bool)

ResolverByName returns a registered ConflictResolver.

type Durability

type Durability int

Durability selects the db/sqlite persistence backend. Its zero value (DurableDefault) means IndexedDB, so callers who want in-memory state must ask for it explicitly.

const (
	// DurableDefault maps to IndexedDB.
	DurableDefault Durability = iota
	// DurableMemory keeps state in wasm memory only (lost on reload).
	DurableMemory
	// DurableIndexedDB snapshots to IndexedDB (the v1 durable backend).
	DurableIndexedDB
	// DurableOPFS is reserved; falls back to IndexedDB in v1.
	DurableOPFS
)

type HydrateMode

type HydrateMode int

HydrateMode controls when a value is loaded from the backend.

const (
	// HydrateEager loads the stored value when the binding mounts.
	HydrateEager HydrateMode = iota
	// HydrateLazy does NOT auto-load the stored value: the binding starts at the supplied initial
	// value and the first Set persists forward over whatever was stored. Use it for write-through
	// state that should not read its prior value back on mount.
	//
	// NOTE: this is "skip the eager load", not "load on first read" — true load-on-first-read is not
	// implemented in the asynchronous-engine hook model (there is no synchronous read path to defer
	// into). Use HydrateEager when you need the stored value available after mount.
	HydrateLazy
)

type Immediate

type Immediate struct{}

Immediate flushes on every write. Simplest and safest; most IndexedDB traffic.

func (Immediate) Close

func (Immediate) Close(parseCtx context.Context) error

func (Immediate) OnWrite

func (Immediate) OnWrite(parseCtx context.Context, parseKey string, parseFlush func(context.Context) error)

type JSONCodec

type JSONCodec struct{}

JSONCodec encodes values as JSON. It is the default.

func (JSONCodec) Decode

func (JSONCodec) Decode(parseData []byte, parseTarget any) error

func (JSONCodec) Encode

func (JSONCodec) Encode(parseValue any) ([]byte, error)

type LastWriteWins

type LastWriteWins struct{}

LastWriteWins keeps the record with the newer UpdatedAt. It is the default.

func (LastWriteWins) Resolve

func (LastWriteWins) Resolve(parseLocal, parseIncoming Record) Record

type Options

type Options struct {
	// Name is the logical database name (db/sqlite Options.Name). Default "gwc".
	Name string
	// Table is the KV table name. Default "gwc_state".
	Table string
	// Codec encodes/decodes values. Default JSONCodec.
	Codec Codec
	// Strategy decides when writes become durable. Default Debounced(150ms).
	Strategy WriteStrategy
	// Conflict reconciles concurrent/cross-tab writes. Default LastWriteWins.
	Conflict ConflictResolver
	// Hydrate controls load timing. Default HydrateEager.
	Hydrate HydrateMode
	// Durability selects the SQLite backend. Default DurableDefault (IndexedDB).
	Durability Durability
	// Backend overrides the persistence layer entirely. When nil, the shared
	// SQLite engine is used.
	Backend PersistenceBackend
}

Options configure a persisted binding. Any zero field takes its default, so the common case needs no Options at all.

type PersistedState

type PersistedState[T any] struct {
	// contains filtered or unexported fields
}

PersistedState is a state handle whose value is durably persisted via the configured backend. On native/SSR builds it degrades to plain in-memory state (the underlying ui.UseEffect is a no-op, so no engine is opened).

func UsePersistedState

func UsePersistedState[T any](parseKey string, parseInitial T, parseOptions ...Options) PersistedState[T]

UsePersistedState returns state durably persisted under parseKey. It starts at parseInitial and asynchronously hydrates from the backend (the value is not available synchronously because the database opens asynchronously). Use Loading() to render a pending state if needed.

func (PersistedState[T]) Err

func (parsePs PersistedState[T]) Err() error

Err returns the last persistence error, or nil.

func (PersistedState[T]) Get

func (parsePs PersistedState[T]) Get() T

Get returns the current value.

func (PersistedState[T]) Loading

func (parsePs PersistedState[T]) Loading() bool

Loading reports whether the initial hydrate from the backend is still pending.

func (PersistedState[T]) Set

func (parsePs PersistedState[T]) Set(parseValue T)

Set updates the value in memory and persists it per the WriteStrategy.

type PersistenceBackend

type PersistenceBackend interface {
	Load(parseCtx context.Context, parseKey string) (parseRecord Record, parseFound bool, parseErr error)
	Save(parseCtx context.Context, parseRecord Record) error
	Delete(parseCtx context.Context, parseKey string) error
	Keys(parseCtx context.Context) ([]string, error)
}

PersistenceBackend is the storage contract behind a binding. The default is the shared SQLite engine; implement this to persist elsewhere (a REST API, a custom OPFS layout, an in-memory mock) while keeping the same hook/atom API.

Cross-tab notification is handled separately (see watch.go), so a backend only needs to implement durable CRUD.

func BackendByName

func BackendByName(parseName string) (PersistenceBackend, bool)

BackendByName returns a registered PersistenceBackend.

type Record

type Record struct {
	Key       string
	Value     []byte
	Version   int64 // monotonic per key
	UpdatedAt int64 // unix milliseconds
}

Record is one stored key/value with the metadata used for conflict resolution.

func Export

func Export(parseCtx context.Context, parseBackend PersistenceBackend) ([]Record, error)

Export reads every record from a backend (egress). The result is a portable snapshot you can JSON/CBOR-encode and POST elsewhere.

type Versioned

type Versioned struct{}

Versioned keeps the record with the higher Version, rejecting stale writes.

func (Versioned) Resolve

func (Versioned) Resolve(parseLocal, parseIncoming Record) Record

type WriteStrategy

type WriteStrategy interface {
	OnWrite(parseCtx context.Context, parseKey string, parseFlush func(context.Context) error)
	Close(parseCtx context.Context) error
}

WriteStrategy decides when a dirty value becomes durable. After each Set the binding calls OnWrite with a flush closure; the strategy chooses when (and how often) to invoke it. Close performs a final flush on teardown.

func Debounced

func Debounced(parseD time.Duration) WriteStrategy

Debounced coalesces bursts of writes, flushing once after d of quiet. Good default for interactive state (typing, sliders).

func OnUnload

func OnUnload() WriteStrategy

OnUnload defers flushing until teardown (Close) and, in the browser, also flushes on page hide. It minimizes writes for state that only needs to survive a reload, not every keystroke.

func StrategyByName

func StrategyByName(parseName string) (WriteStrategy, bool)

StrategyByName returns a registered WriteStrategy.

Jump to

Keyboard shortcuts

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