Documentation
¶
Overview ¶
Package nodra is a state library for MyGo apps: concurrency-safe reactive stores whose snapshots drive native MyGo views. Invalidate redraws a window when its store publishes; the Store itself does not depend on a running MyGo app, so services and tests can use it as well.
Index ¶
- Variables
- func Atomic2[A, B any](first *Store[A], second *Store[B], fn func(*A, *B) error) (err error)
- func AtomicGroup(ops ...AtomicOp) (err error)
- func Invalidate[T any](source Observable[T], window *mygo.Window) (stop func())
- func InvalidateProjection[T, V any](source Reader[T], window *mygo.Window, project func(T) V) func()
- func InvalidateProjectionWith[T, V any](source Reader[T], window *mygo.Window, project func(T) V, ...) func()
- func InvalidateSelector[T any, V any](source Observable[T], window *mygo.Window, selector func(T) V) (stop func())
- func InvalidateSelectorWith[T any, V any](source Observable[T], window *mygo.Window, selector func(T) V, ...) (stop func())
- func InvalidateTarget[T any](source Observable[T], target InvalidationTarget) func()
- func SubscribeProjection[T, V any](source Reader[T], project func(T) V, fn func(V)) func()
- func SubscribeProjectionWith[T, V any](source Reader[T], project func(T) V, fn func(current, previous V), ...) func()
- func SubscribeSelector[T any, S any](store Observable[T], selector func(T) S, fn func(S)) func()
- func SubscribeSelectorWith[T any, V any](source Observable[T], selector func(T) V, fn func(current, previous V), ...) func()
- type AESGCMCodec
- type ActionEvent
- type ActionHooks
- type ActionMetrics
- type ActionOptions
- type ActionPolicy
- type ActionTrace
- type AsyncAction
- func (a *AsyncAction[T, I, R]) Close(ctx context.Context) error
- func (a *AsyncAction[T, I, R]) ExportMetrics(sink MetricsSink) error
- func (a *AsyncAction[T, I, R]) Metrics() ActionMetrics
- func (a *AsyncAction[T, I, R]) Run(ctx context.Context, input I) (*Task, error)
- func (a *AsyncAction[T, I, R]) Status() ReadOnly[AsyncState]
- func (a *AsyncAction[T, I, R]) SubscribeMetrics(fn func(ActionMetrics)) func()
- type AsyncOptions
- type AsyncPersistence
- type AsyncState
- type AtomicOp
- type CheckedFileStorage
- type Codec
- type Definition
- type Derived
- func Combine[A, B, V any](first Observable[A], second Observable[B], project func(A, B) V, ...) *Derived[Pair[A, B], V]
- func Derive[T any, V any](source Observable[T], selector func(T) V, equals func(V, V) bool) *Derived[T, V]
- func DeriveProjection[T any, V any](source Reader[T], project func(T) V, equals func(V, V) bool) *Derived[T, V]
- type Field
- type FileConflictError
- type FileFingerprint
- type FileStorage
- type History
- type HistoryOptions
- type HistoryState
- type InvalidationTarget
- type JSONCodec
- type JSONMigration
- type KeyProvider
- type LoadError
- type MemoryKeyProvider
- type MetricsSink
- type Mutation
- type MutationType
- type Observable
- type Pair
- type PanicError
- type Persistence
- func (p *Persistence[T]) AutoSave(ctx context.Context) func()
- func (p *Persistence[T]) AutoSaveAsync(ctx context.Context, options AsyncOptions) (*AsyncPersistence[T], error)
- func (p *Persistence[T]) AutoSaveWithErrors(ctx context.Context) (<-chan error, func())
- func (p *Persistence[T]) Load(ctx context.Context) error
- func (p *Persistence[T]) LoadBackup(ctx context.Context) error
- func (p *Persistence[T]) LoadWithRecovery(ctx context.Context) error
- func (p *Persistence[T]) Save(ctx context.Context) error
- type PersistenceOptions
- type PersistenceState
- type ReadOnly
- func (r ReadOnly[T]) ActiveTasks() []ActionTrace
- func (r ReadOnly[T]) Done() <-chan struct{}
- func (r ReadOnly[T]) Get() T
- func (r ReadOnly[T]) ID() string
- func (r ReadOnly[T]) Snapshot() Snapshot[T]
- func (r ReadOnly[T]) State() T
- func (r ReadOnly[T]) Subscribe(fn func(Snapshot[T])) func()
- func (r ReadOnly[T]) SubscribeActionTrace(fn func(ActionTrace)) func()
- func (r ReadOnly[T]) SubscribeActions(fn func(ActionEvent)) func()
- func (r ReadOnly[T]) SubscribeMutations(fn func(Mutation[T])) func()
- func (r ReadOnly[T]) SubscribeSnapshot(fn func(Snapshot[T])) func()
- func (r ReadOnly[T]) SubscribeWith(options SubscribeOptions[T], fn func(Snapshot[T])) func()
- func (r ReadOnly[T]) TaskSnapshot() TaskSnapshot
- func (r ReadOnly[T]) Version() uint64
- func (r ReadOnly[T]) Watch(ctx context.Context, buffer int) <-chan Snapshot[T]
- type Reader
- type RecoveryError
- type RetryOptions
- type Scope
- type SelectorOptions
- type Snapshot
- type Storage
- type Store
- func (s *Store[T]) Action(name string, fn func(*T) error) func() error
- func (s *Store[T]) ActiveTasks() []ActionTrace
- func (s *Store[T]) Batch(fn func()) error
- func (s *Store[T]) BatchErr(fn func() error) error
- func (s *Store[T]) Close() error
- func (s *Store[T]) Do(name string, fn func(*T) error) error
- func (s *Store[T]) Done() <-chan struct{}
- func (s *Store[T]) Get() T
- func (s *Store[T]) ID() string
- func (s *Store[T]) Patch(fn func(*T)) error
- func (s *Store[T]) ReadOnly() ReadOnly[T]
- func (s *Store[T]) Reset() error
- func (s *Store[T]) Set(value T) error
- func (s *Store[T]) Snapshot() Snapshot[T]
- func (s *Store[T]) State() T
- func (s *Store[T]) Subscribe(fn func(Snapshot[T])) func()
- func (s *Store[T]) SubscribeActionHooks(hooks ActionHooks) func()
- func (s *Store[T]) SubscribeActionTrace(fn func(ActionTrace)) func()
- func (s *Store[T]) SubscribeActions(fn func(ActionEvent)) func()
- func (s *Store[T]) SubscribeMutations(fn func(Mutation[T])) func()
- func (s *Store[T]) SubscribeSnapshot(fn func(Snapshot[T])) func()
- func (s *Store[T]) SubscribeWith(options SubscribeOptions[T], fn func(Snapshot[T])) func()
- func (s *Store[T]) TaskSnapshot() TaskSnapshot
- func (s *Store[T]) Transaction(fn func(*T) error) error
- func (s *Store[T]) Update(fn func(*T)) error
- func (s *Store[T]) UpdateErr(fn func(*T) error) error
- func (s *Store[T]) Version() uint64
- func (s *Store[T]) Watch(ctx context.Context, buffer int) <-chan Snapshot[T]
- type SubscribeOptions
- type Task
- type TaskMetrics
- type TaskPhase
- type TaskSnapshot
- type VersionedJSONCodec
Examples ¶
Constants ¶
This section is empty.
Variables ¶
var ( ErrNoUndo = errors.New("nodra: nothing to undo") ErrNoRedo = errors.New("nodra: nothing to redo") )
ErrNoUndo and ErrNoRedo report an empty history stack.
var ( ErrSuperseded = errors.New("nodra: async action superseded") ErrInvalidAction = errors.New("nodra: invalid async action") ErrQueueFull = errors.New("nodra: async action queue full") )
var ( ErrUnsupportedVersion = errors.New("nodra: unsupported schema version") ErrMissingMigration = errors.New("nodra: missing schema migration") )
var ErrClosed = errors.New("nodra: store is closed")
var ErrFileConflict = errors.New("nodra: file content changed since last observation")
var ErrHistoryBatch = errors.New("nodra: cannot undo or redo inside an open batch")
ErrHistoryBatch reports travel without an independent publication boundary.
var ErrHistoryGroup = errors.New("nodra: cannot undo or redo inside an open history group")
var ErrInvalidBinding = errors.New("nodra: invalid field binding")
var ErrInvalidHistory = errors.New("nodra: invalid history size configuration")
var ErrInvalidKey = errors.New("nodra: invalid encryption key")
var ErrInvalidPersistence = errors.New("nodra: invalid persistence configuration")
var ErrNoBackup = errors.New("nodra: backup storage unavailable")
var ErrPersistenceRecovery = errors.New("nodra: primary and backup recovery failed")
ErrPersistenceRecovery marks a failure to load both the primary and backup candidates.
Functions ¶
func Atomic2 ¶ added in v0.9.0
Atomic2 commits two different stores as one visibility boundary. All source values are prepared while both store locks are held; a callback error or panic leaves both stores unchanged. Readers and ordinary writers wait until both values are committed. Notifications run after the boundary is released, so listeners may safely reenter either store. The callback must not call Store methods directly; use the provided pointers.
func AtomicGroup ¶ added in v0.9.0
AtomicGroup commits any number of typed Store updates as one visibility boundary. Every callback is prepared while all participating store locks are held; an error or panic leaves every store unchanged. Lock order is stable across calls, so groups built in opposite order cannot deadlock. Notifications run after all locks are released and may safely reenter their stores.
func Invalidate ¶ added in v0.2.0
func Invalidate[T any](source Observable[T], window *mygo.Window) (stop func())
Invalidate requests a new frame of window's native Content whenever store publishes a snapshot. The subscription stops when the window closes; the returned function stops it earlier and is safe to call repeatedly. Nil inputs and destroyed windows subscribe to nothing. A callback already running when stopped may still request a frame.
func InvalidateProjection ¶ added in v0.6.0
func InvalidateProjection[T, V any](source Reader[T], window *mygo.Window, project func(T) V) func()
InvalidateProjection requests frames only when an owned projection changes, without cloning the entire source. The projector and callbacks follow SubscribeProjection's ownership rules. The window owns the subscription lifetime.
func InvalidateProjectionWith ¶ added in v0.6.0
func InvalidateProjectionWith[T, V any](source Reader[T], window *mygo.Window, project func(T) V, options SelectorOptions[V]) func()
func InvalidateSelector ¶ added in v0.5.0
func InvalidateSelector[T any, V any](source Observable[T], window *mygo.Window, selector func(T) V) (stop func())
InvalidateSelector requests a frame only when the selected value changes, using SubscribeSelector's equality rules. The subscription stops when the window closes or the returned function is called. Selectors run on writer goroutines and must be concurrency-safe; selected reference values follow the store's ownership policy.
func InvalidateSelectorWith ¶ added in v0.5.0
func InvalidateSelectorWith[T any, V any](source Observable[T], window *mygo.Window, selector func(T) V, options SelectorOptions[V]) (stop func())
InvalidateSelectorWith is InvalidateSelector with custom equality and optional immediate invalidation. Nil inputs and destroyed windows subscribe to nothing. As with Invalidate, stopping does not wait for callbacks already running.
func InvalidateTarget ¶ added in v0.9.0
func InvalidateTarget[T any](source Observable[T], target InvalidationTarget) func()
InvalidateTarget requests a redraw from any InvalidationTarget whenever the source publishes. It accepts read-only and derived sources without exposing a concrete UI toolkit. The caller owns the target lifecycle and should call the returned stop function when the target is no longer valid.
func SubscribeProjection ¶ added in v0.6.0
func SubscribeProjectionWith ¶ added in v0.6.0
func SubscribeProjectionWith[T, V any](source Reader[T], project func(T) V, fn func(current, previous V), options SelectorOptions[V]) func()
SubscribeProjectionWith is an opt-in selector that borrows immutable source snapshots instead of cloning the entire Store per notification. project must be pure and concurrency-safe, must not retain or mutate source references, and must return independently owned projections. Callbacks must treat the current/previous projections as immutable. Equality, initialization and callback ordering match SubscribeSelectorWith. On default stores, source reference values must remain immutable as with any retained snapshot.
func SubscribeSelector ¶
func SubscribeSelector[T any, S any](store Observable[T], selector func(T) S, fn func(S)) func()
func SubscribeSelectorWith ¶
func SubscribeSelectorWith[T any, V any](source Observable[T], selector func(T) V, fn func(current, previous V), options SelectorOptions[V]) func()
SubscribeSelectorWith calls fn with current and previous selected values. It catches up after registration and ignores obsolete computations. Callbacks run on writer goroutines and may overlap, as with Store.Subscribe.
Types ¶
type AESGCMCodec ¶ added in v0.9.0
type AESGCMCodec[T any] struct { Inner Codec[T] Keys KeyProvider }
AESGCMCodec encrypts an inner codec's bytes with a random nonce and stores only the key ID, nonce and ciphertext in a JSON envelope. It supports key rotation by reading historical IDs from the provider while new Encode calls use Current.
func (AESGCMCodec[T]) Decode ¶ added in v0.9.0
func (c AESGCMCodec[T]) Decode(data []byte) (T, error)
func (AESGCMCodec[T]) Encode ¶ added in v0.9.0
func (c AESGCMCodec[T]) Encode(value T) ([]byte, error)
type ActionEvent ¶
type ActionEvent struct {
Metrics TaskMetrics
ID uint64
RunID uint64
Key string
Version uint64
Committed bool
StoreID string
Name string
Started time.Time
Finished time.Time
Duration time.Duration
Err error
}
ActionEvent describes one completed action.
type ActionHooks ¶
type ActionHooks struct {
Before func(ActionEvent)
After func(ActionEvent)
Error func(ActionEvent)
}
ActionHooks observes action lifecycle events. Before runs before the mutation; After runs only on success; Error runs only on failure.
type ActionMetrics ¶ added in v0.9.0
type ActionMetrics struct {
Accepted uint64
Completed uint64
Succeeded uint64
Failed uint64
Canceled uint64
Retried uint64
QueuePeak int
DurationSum time.Duration
DurationMax time.Duration
}
ActionMetrics aggregates terminal outcomes for one AsyncAction without retaining completed Task values. Duration totals include each run once; Retries counts additional work attempts.
type ActionOptions ¶ added in v0.6.0
type ActionOptions[I any] struct { Policy ActionPolicy Key func(I) string MaxConcurrent int QueueLimit int // Debounce delays LatestWins work per key until this interval after acceptance. Superseded inputs never start work. Zero preserves immediate scheduling; QueueAll rejects a positive delay. Debounce time.Duration Retry RetryOptions }
ActionOptions configures cooperative work scheduling. LatestWins (default) replaces only the preceding task with the same Key; nil Key uses one shared key. QueueAll keeps every accepted task. MaxConcurrent bounds work including canceled work that has not returned; zero is unbounded for LatestWins and defaults to one for QueueAll. QueueLimit bounds waiting tasks (default 64 when bounded). Starts are FIFO; with concurrency above one commits follow completion order. Key must be pure and concurrency-safe.
type ActionPolicy ¶ added in v0.6.0
type ActionPolicy uint8
const ( LatestWins ActionPolicy = iota QueueAll )
type ActionTrace ¶ added in v0.6.0
type ActionTrace struct {
Metrics TaskMetrics
ID uint64
RunID uint64
StoreID string
Name string
Key string
Phase TaskPhase
Progress float64
Revision uint64
TaskVersion uint64
Version uint64
Committed bool
At time.Time
Duration time.Duration
Err error
}
ActionTrace observes asynchronous task lifecycles. ID is the source Store action ID; RunID is local to its AsyncAction. Revision orders trace notifications even when callbacks overlap. Version identifies a published commit when Committed is true. Progress is monotonic for one task. Completion ActionEvents remain a separate, backward-compatible stream.
type AsyncAction ¶ added in v0.6.0
type AsyncAction[T, I, R any] struct { // contains filtered or unexported fields }
AsyncAction runs work outside the Store lock with an explicit commit policy. The default accepts only the latest uncanceled run per key. Input and result ownership follows Store's policy; copy mutable inputs before Run. Work must honor its context. Progress, status subscribers and action hooks can run on background goroutines.
func NewAsyncAction ¶ added in v0.6.0
func NewAsyncActionWithOptions ¶ added in v0.6.0
func (*AsyncAction[T, I, R]) Close ¶ added in v0.6.0
func (a *AsyncAction[T, I, R]) Close(ctx context.Context) error
Close prevents new runs, cancels every active run and waits for cooperative work within ctx. The status store closes after the final run's terminal status. A timed-out Close can be called again to wait for remaining work. Do not Close synchronously from work, hooks or subscribers of this action; cancel there and close from the owner instead.
func (*AsyncAction[T, I, R]) ExportMetrics ¶ added in v0.9.0
func (a *AsyncAction[T, I, R]) ExportMetrics(sink MetricsSink) error
ExportMetrics records the current aggregate metrics for one action. It takes a snapshot under the action lock and invokes the sink afterward, so sinks may reenter the action or perform blocking export work. A nil sink is rejected.
func (*AsyncAction[T, I, R]) Metrics ¶ added in v0.9.0
func (a *AsyncAction[T, I, R]) Metrics() ActionMetrics
func (*AsyncAction[T, I, R]) Run ¶ added in v0.6.0
func (a *AsyncAction[T, I, R]) Run(ctx context.Context, input I) (*Task, error)
Run accepts work according to ActionOptions. LatestWins supersedes the preceding task with the same key; QueueAll retains every accepted task. Canceled and superseded results never commit. ErrQueueFull rejects work without canceling existing tasks.
func (*AsyncAction[T, I, R]) Status ¶ added in v0.6.0
func (a *AsyncAction[T, I, R]) Status() ReadOnly[AsyncState]
Status returns the observable status of the latest run, suitable for MyGo invalidation or DevTools attachment. Callers must not write to this store.
func (*AsyncAction[T, I, R]) SubscribeMetrics ¶ added in v0.9.0
func (a *AsyncAction[T, I, R]) SubscribeMetrics(fn func(ActionMetrics)) func()
SubscribeMetrics observes aggregate metrics after each terminal task. The callback runs outside the action lock and is protected from panics so an exporter cannot strand a task or terminate the worker goroutine. The returned function is idempotent.
type AsyncOptions ¶ added in v0.3.0
AsyncOptions configures write-behind persistence. Debounce is a trailing-edge delay for bursts of updates; zero starts immediately. Flush bypasses the delay.
type AsyncPersistence ¶ added in v0.3.0
type AsyncPersistence[T any] struct { // contains filtered or unexported fields }
AsyncPersistence owns one worker and a bounded error stream. It coalesces updates without blocking writer goroutines on encoding or storage. Failed writes remain pending; the next update or Flush retries them without spinning. Values must be immutable or the store must use NewWithClone.
func (*AsyncPersistence[T]) Close ¶ added in v0.3.0
func (a *AsyncPersistence[T]) Close(ctx context.Context) error
Close stops observing updates, flushes the final observed state, then stops the worker even if the flush fails. It is idempotent; concurrent callers wait on the same result with their own context. Do not mutate the store concurrently with Close if those changes must be included. Retry failed shutdown writes with Persistence.Save; a closed async writer cannot be restarted.
func (*AsyncPersistence[T]) Errors ¶ added in v0.3.0
func (a *AsyncPersistence[T]) Errors() <-chan error
Errors reports failed write attempts. The bounded stream may drop errors if unread; Flush and Close return their write error directly. It closes on exit.
func (*AsyncPersistence[T]) Flush ¶ added in v0.3.0
func (a *AsyncPersistence[T]) Flush(ctx context.Context) error
Flush waits until the version visible when called has been saved, bypassing debounce and retrying a previous failure. Canceling this wait does not cancel the shared worker. Later updates may be included in the same write.
func (*AsyncPersistence[T]) Status ¶ added in v0.7.0
func (a *AsyncPersistence[T]) Status() ReadOnly[PersistenceState]
Status exposes a read-only observable suitable for MyGo invalidation and DevTools. Status publications are serialized while remaining outside writer locks, so subscriber completion order follows status revisions. Do not Flush or Close this writer from its own subscribers. Subscriber panics cannot stop persistence or shutdown.
type AsyncState ¶ added in v0.6.0
type AsyncState struct {
Metrics TaskMetrics
ID uint64
Name string
Running bool
Progress float64
Error string
Phase TaskPhase
Key string
Revision uint64
}
AsyncState describes the latest run. Progress is between zero and one; Error is diagnostic text. Treat the action's status store as read-only.
type AtomicOp ¶ added in v0.9.0
type AtomicOp struct {
// contains filtered or unexported fields
}
AtomicOp is one typed update in an AtomicGroup. Create operations with AtomicUpdate; the fields are intentionally private so an operation cannot be constructed without the store lock and rollback guarantees.
func AtomicUpdate ¶ added in v0.9.0
AtomicUpdate prepares one store update for an AtomicGroup. The callback sees a private candidate and may return an error to abort the complete group. It must not call Store methods directly; use the provided pointer instead.
type CheckedFileStorage ¶ added in v0.7.0
type CheckedFileStorage struct {
// contains filtered or unexported fields
}
CheckedFileStorage opts into content-conflict detection for a FileStorage destination. Use the returned pointer without copying it. Cooperating instances serialize checks and replacements through a persistent Path+".lock" sidecar. Never remove that sidecar while any instance may be active. Plain FileStorage and editors do not honor the advisory lock: their edits completed before the check are detected, but a concurrent uncooperative write can race the replacement. Use regular local files and the same canonical destination; hard-link aliases and network filesystem locking are outside this contract.
func (*CheckedFileStorage) Load ¶ added in v0.7.0
func (c *CheckedFileStorage) Load(ctx context.Context) (data []byte, err error)
Load explicitly accepts the bytes read as the new baseline. A missing primary acknowledges absence and still returns os.ErrNotExist. Decode/validation remains Persistence's responsibility; stop on those errors before starting autosave.
func (*CheckedFileStorage) LoadBackup ¶ added in v0.7.0
func (c *CheckedFileStorage) LoadBackup(ctx context.Context) (data []byte, err error)
LoadBackup reads the backup without acknowledging changed primary content. Recovery does not silently authorize replacing external primary edits.
func (*CheckedFileStorage) Save ¶ added in v0.7.0
func (c *CheckedFileStorage) Save(ctx context.Context, data []byte) error
Save rejects content that differs from the last successful Load or primary replacement. A new instance uses the constructor's baseline. A failed backup can follow a successful primary write; that primary is acknowledged so a retry does not conflict with this instance's own write. Conflicts leave primary and backup untouched and remain conflicts until the application explicitly loads/reconciles or chooses another destination.
type Definition ¶
type Definition[T any] struct { // contains filtered or unexported fields }
Definition is a reusable named store factory, analogous to Pinia's defineStore and Zustand's reusable vanilla store creator.
func Define ¶
func Define[T any](id string, initial func() T) *Definition[T]
Define creates a store definition. Each New call gets independent state.
func DefineWithClone ¶ added in v0.3.0
func DefineWithClone[T any](id string, initial func() T, clone func(T) T) *Definition[T]
DefineWithClone creates a named factory with NewWithClone's isolation policy.
func (*Definition[T]) ID ¶
func (d *Definition[T]) ID() string
func (*Definition[T]) New ¶
func (d *Definition[T]) New() *Store[T]
type Derived ¶
Derived is a memoized getter derived from an observable value. It recomputes when the source publishes and only notifies listeners when the selected value changes. Reference values and listener arguments remain read-only.
func Combine ¶ added in v0.7.0
func Combine[A, B, V any](first Observable[A], second Observable[B], project func(A, B) V, equals func(V, V) bool) *Derived[Pair[A, B], V]
Combine derives a value from two stores or derived values. Independent writes are eventually consistent, while Atomic2 and AtomicGroup transaction notifications are settled through the derived graph before downstream callbacks run. Old source versions and obsolete computations cannot replace newer ones. Close the returned derived value to detach both sources. Projectors/comparators must be pure, concurrency-safe and keep source references immutable.
func Derive ¶
func Derive[T any, V any](source Observable[T], selector func(T) V, equals func(V, V) bool) *Derived[T, V]
func DeriveProjection ¶ added in v0.6.0
func DeriveProjection[T any, V any](source Reader[T], project func(T) V, equals func(V, V) bool) *Derived[T, V]
DeriveProjection memoizes an owned projection without whole-source clones. It follows SubscribeProjection's ownership contract; returned reference values and listener arguments are read-only.
func (*Derived[T, V]) Snapshot ¶ added in v0.7.0
Snapshot returns the memoized value, its accepted source version and the cross-store transaction that produced it. Derived reference values are read-only; equality-suppressed changes still advance the version without notifying.
func (*Derived[T, V]) SubscribeSnapshot ¶ added in v0.7.0
type Field ¶ added in v0.7.0
type Field[T, V any] struct { // contains filtered or unexported fields }
Field binds an immediate editor to one field of shared state. A write runs against the latest Store candidate, preserving unrelated concurrent changes. Read/write callbacks follow the Store ownership policy; use NewWithClone for mutable references. Keep form drafts separate when edits require explicit Save/Cancel.
func (Field[T, V]) Edit ¶ added in v0.7.0
Edit draws/edits a local field value and commits it only when edit reports a change. The editor runs outside Store locks, so it can render MyGo controls. Validation errors are returned to the owner; failed writes preserve shared state.
type FileConflictError ¶ added in v0.7.0
type FileConflictError struct {
Path string
Expected FileFingerprint
Actual FileFingerprint
}
FileConflictError reports conflicting content without embedding file data. Rejected saves leave primary and backup bytes unchanged. Load explicitly acknowledges current primary content; use a different destination to preserve both versions instead.
func (*FileConflictError) Error ¶ added in v0.7.0
func (e *FileConflictError) Error() string
func (*FileConflictError) Unwrap ¶ added in v0.7.0
func (e *FileConflictError) Unwrap() error
type FileFingerprint ¶ added in v0.7.0
FileFingerprint identifies the bytes observed at a destination. Exists distinguishes a missing file from an empty file. Equal-content replacements and changes reverted before the next check are indistinguishable.
type FileStorage ¶
type FileStorage struct {
Path string
Perm os.FileMode
// Backup writes an independent Path+".bak" copy after each successful primary replacement. A backup failure is returned even though the primary may already contain the new state. Use one Persistence writer per destination.
Backup bool
// Durable syncs the containing directory after each rename. This makes the file name update durable across a crash on filesystems that support directory fsync. It may add latency and is opt-in for compatibility.
Durable bool
// Journal enables a two-phase crash recovery record. The journal is committed
// only after the primary rename and directory sync; an unfinished record is
// discarded on the next Load. It adds extra filesystem writes and is opt-in.
Journal bool
}
FileStorage stores encoded state at Path. Save writes a temporary file in the same directory and renames it into place, so readers see a complete file.
func (FileStorage) LoadBackup ¶ added in v0.6.0
func (f FileStorage) LoadBackup(ctx context.Context) ([]byte, error)
func (FileStorage) WithConflictDetection ¶ added in v0.7.0
func (f FileStorage) WithConflictDetection(ctx context.Context) (*CheckedFileStorage, error)
WithConflictDetection creates the parent directory/lock sidecar and observes current primary bytes as the initial baseline, including absence. It does not load or validate Store state. Parent directory aliases are canonicalized; primary symlinks and nonregular files are rejected. All cooperating writers must opt in; ordinary FileStorage behavior remains unchanged.
type History ¶
type History[T any] struct { // contains filtered or unexported fields }
History records successful store snapshots and provides bounded undo/redo.
func Track ¶
Track starts recording changes to store. limit is the maximum number of undo entries; values below one disable history recording.
func TrackWith ¶ added in v0.6.0
func TrackWith[T any](store *Store[T], options HistoryOptions[T]) *History[T]
func (*History[T]) BeginGroup ¶ added in v0.6.0
BeginGroup merges edits across frames into one undo step while still publishing every update. Groups nest; the returned end function is idempotent. Concurrent writers join the group. Undo and Redo are rejected until all groups end. Filtering still applies, so a group containing only ignored changes adds no step.
func (*History[T]) Close ¶
Close stops recording future changes. It does not close the underlying store.
func (*History[T]) Status ¶ added in v0.7.0
func (h *History[T]) Status() ReadOnly[HistoryState]
type HistoryOptions ¶ added in v0.6.0
type HistoryOptions[T any] struct { Limit int // MaxBytes budgets estimated undo and redo payloads together; zero disables sizing. Positive budgets require Size. The current value, Store state, shared allocations and metadata are outside this estimate. MaxBytes int64 // Size estimates one retained value. It must return a nonnegative value and follow Equals/Restore purity and lock rules. Invalid configurations or negative estimates panic with ErrInvalidHistory. Size func(T) int64 Equals func(previous, next T) bool Restore func(current, recorded T) T }
HistoryOptions configures recording. Equals excludes changes to irrelevant fields. Restore merges recorded fields into current state so ignored fields survive travel. Both functions must be pure, must not mutate arguments, and must not reenter the Store or History; they can run while those locks are held. With nil callbacks, every publication records and travel replaces the whole value.
type HistoryState ¶ added in v0.7.0
type HistoryState struct {
Revision uint64
UndoEntries int
RedoEntries int
Bytes int64
MaxBytes int64
Dropped uint64
Grouped bool
Closed bool
CanUndo bool
CanRedo bool
}
HistoryState describes history availability and estimated retained payloads. Revision orders status publications independently of source versions. Bytes excludes the current value and is an application estimate, not process heap usage. CanUndo/CanRedo are false while grouped or closed.
type InvalidationTarget ¶ added in v0.9.0
type InvalidationTarget interface {
Invalidate()
}
InvalidationTarget is the minimal redraw capability required by InvalidateTarget. MyGo Window satisfies it, as do test fakes and other native UI hosts. Implementations must make Invalidate safe to call from a Store subscriber goroutine.
type JSONCodec ¶
type JSONCodec[T any] struct{}
JSONCodec encodes a store value using encoding/json.
type JSONMigration ¶ added in v0.3.0
type JSONMigration func(json.RawMessage) (json.RawMessage, error)
JSONMigration converts one schema version to the next. Inputs and outputs are state JSON, without the version envelope. Return an error to abort Load.
type KeyProvider ¶ added in v0.9.0
type KeyProvider interface {
Current() (id string, key []byte, err error)
Get(id string) ([]byte, error)
}
KeyProvider supplies the current key for new writes and historical keys for reads. Implementations should keep key material outside persisted state and rotate by adding the new key before making it current.
type LoadError ¶ added in v0.6.0
LoadError identifies the failing load stage without changing state or files. Backup distinguishes explicit backup loads. Unwrap preserves codec, validation and filesystem errors.
type MemoryKeyProvider ¶ added in v0.9.0
type MemoryKeyProvider struct {
// contains filtered or unexported fields
}
MemoryKeyProvider is a small concurrency-safe provider for tests and local applications. Production applications should implement KeyProvider over their OS keychain or secret manager.
func NewMemoryKeyProvider ¶ added in v0.9.0
func NewMemoryKeyProvider(id string, key []byte) (*MemoryKeyProvider, error)
func (*MemoryKeyProvider) Add ¶ added in v0.9.0
func (p *MemoryKeyProvider) Add(id string, key []byte) error
func (*MemoryKeyProvider) Current ¶ added in v0.9.0
func (p *MemoryKeyProvider) Current() (string, []byte, error)
func (*MemoryKeyProvider) Get ¶ added in v0.9.0
func (p *MemoryKeyProvider) Get(id string) ([]byte, error)
func (*MemoryKeyProvider) SetCurrent ¶ added in v0.9.0
func (p *MemoryKeyProvider) SetCurrent(id string) error
type MetricsSink ¶ added in v0.9.0
type MetricsSink interface {
RecordActionMetrics(name string, metrics ActionMetrics)
}
MetricsSink is the dependency-free bridge for exporting Nodra action metrics. Adapters can translate one snapshot into Prometheus gauges, OpenTelemetry instruments, logs, or an application's existing telemetry system without making the core package depend on a telemetry SDK.
type Mutation ¶
type Mutation[T any] struct { ActionID uint64 TransactionID uint64 Type MutationType Name string Previous T Value T Version uint64 At time.Time }
Mutation contains Pinia-like subscription metadata.
type MutationType ¶
type MutationType string
MutationType describes why a store published a snapshot.
const ( MutationDirect MutationType = "direct" MutationPatch MutationType = "patch" MutationAction MutationType = "action" MutationReset MutationType = "reset" MutationTransaction MutationType = "transaction" )
type Observable ¶ added in v0.7.0
type Observable[T any] interface { Snapshot() Snapshot[T] SubscribeSnapshot(func(Snapshot[T])) func() }
Observable is a versioned value source shared by stores, read-only views and derived values. Callbacks may overlap and must treat reference values as read-only. Implementations must use monotonically increasing versions and subscribe without replay; constructors catch up after subscribing.
type Pair ¶ added in v0.7.0
type Pair[A, B any] struct { First A Second B }
Pair contains the latest observed values of two independent sources.
type PanicError ¶ added in v0.6.0
type PanicError struct{ Value any }
PanicError reports a panic at the asynchronous user-code boundary. The Store may already have committed if a notification or completion hook panicked.
func (*PanicError) Error ¶ added in v0.6.0
func (e *PanicError) Error() string
type Persistence ¶
type Persistence[T any] struct { // contains filtered or unexported fields }
Persistence connects a store to a Storage and Codec. It is explicit by default: call Load at startup and Save after successful application actions. Save calls on the same instance are serialized, including state capture and encoding. Separate instances sharing a storage destination are not coordinated.
func NewPersistence ¶
func NewPersistence[T any](store *Store[T], storage Storage, codec Codec[T]) *Persistence[T]
func NewPersistenceWithOptions ¶ added in v0.6.0
func NewPersistenceWithOptions[T any](store *Store[T], storage Storage, codec Codec[T], options PersistenceOptions[T]) *Persistence[T]
func (*Persistence[T]) AutoSave ¶
func (p *Persistence[T]) AutoSave(ctx context.Context) func()
AutoSave subscribes to updates and saves synchronously after each update. The returned function stops autosaving. Use Batch to coalesce writes.
func (*Persistence[T]) AutoSaveAsync ¶ added in v0.3.0
func (p *Persistence[T]) AutoSaveAsync(ctx context.Context, options AsyncOptions) (*AsyncPersistence[T], error)
AutoSaveAsync subscribes and schedules the current state once, then all subsequent versions. Use Close before closing the store to drain writes. Parent cancellation and store closure stop the worker without a guaranteed drain. Storage.Save must respect context cancellation to bound shutdown time.
func (*Persistence[T]) AutoSaveWithErrors ¶
func (p *Persistence[T]) AutoSaveWithErrors(ctx context.Context) (<-chan error, func())
AutoSaveWithErrors subscribes to updates and reports persistence failures on a buffered channel. The channel closes when ctx is canceled or stop is called. Stop releases the background watcher but does not wait for in-flight saves.
func (*Persistence[T]) LoadBackup ¶ added in v0.6.0
func (p *Persistence[T]) LoadBackup(ctx context.Context) error
LoadBackup explicitly restores validated backup state to memory. It never rewrites the primary file, so a corrupt original remains available for diagnosis. Call before starting autosave; a later Save intentionally writes the current state to disk.
func (*Persistence[T]) LoadWithRecovery ¶ added in v0.9.0
func (p *Persistence[T]) LoadWithRecovery(ctx context.Context) error
LoadWithRecovery explicitly tries the primary first and then the backup when the primary cannot be read, decoded or validated. It never rewrites either file and never hides a failure: if both candidates fail, RecoveryError retains both causes. Cancellation and a closed Store stop before attempting the backup.
type PersistenceOptions ¶ added in v0.6.0
PersistenceOptions validates decoded and saved state before it reaches the Store or storage. Validate must be pure and concurrency-safe, and must not mutate its input.
type PersistenceState ¶ added in v0.7.0
type PersistenceState struct {
Revision uint64
RequestedVersion uint64
SavedVersion uint64
HasSaved bool
Pending bool
Saving bool
Closed bool
Error string
}
PersistenceState describes this async writer's durable acknowledgement, not writes made through other Persistence instances. HasSaved distinguishes an unsaved initial version zero. Pending remains true after failures and shutdown with unsaved changes. Error is the last write failure, cleared on retry. Revision orders publications independently of Store versions.
type ReadOnly ¶ added in v0.6.0
type ReadOnly[T any] struct { // contains filtered or unexported fields }
ReadOnly grants observation without Set, Update, Reset, action hooks or Close. Values still follow the source's cloning/immutability policy; this is a capability boundary, not a deep-freeze operation. Its zero value is an empty, invalid source (Done returns nil).
func (ReadOnly[T]) ActiveTasks ¶ added in v0.6.0
func (r ReadOnly[T]) ActiveTasks() []ActionTrace
func (ReadOnly[T]) SubscribeActionTrace ¶ added in v0.6.0
func (r ReadOnly[T]) SubscribeActionTrace(fn func(ActionTrace)) func()
func (ReadOnly[T]) SubscribeActions ¶ added in v0.6.0
func (r ReadOnly[T]) SubscribeActions(fn func(ActionEvent)) func()
func (ReadOnly[T]) SubscribeMutations ¶ added in v0.6.0
func (ReadOnly[T]) SubscribeSnapshot ¶ added in v0.7.0
func (ReadOnly[T]) SubscribeWith ¶ added in v0.6.0
func (r ReadOnly[T]) SubscribeWith(options SubscribeOptions[T], fn func(Snapshot[T])) func()
func (ReadOnly[T]) TaskSnapshot ¶ added in v0.6.0
func (r ReadOnly[T]) TaskSnapshot() TaskSnapshot
type Reader ¶ added in v0.6.0
type Reader[T any] interface { Get() T State() T Snapshot() Snapshot[T] SubscribeSnapshot(func(Snapshot[T])) func() Version() uint64 ID() string Done() <-chan struct{} Subscribe(func(Snapshot[T])) func() SubscribeWith(SubscribeOptions[T], func(Snapshot[T])) func() SubscribeMutations(func(Mutation[T])) func() SubscribeActions(func(ActionEvent)) func() SubscribeActionTrace(func(ActionTrace)) func() ActiveTasks() []ActionTrace TaskSnapshot() TaskSnapshot Watch(context.Context, int) <-chan Snapshot[T] // contains filtered or unexported methods }
Reader is the observation surface shared by Store and ReadOnly. Only Nodra sources implement it; a read-only wrapper never exposes its backing Store. Snapshot ownership and callback concurrency are unchanged.
type RecoveryError ¶ added in v0.9.0
RecoveryError preserves both the primary load failure and the backup load failure returned by LoadWithRecovery. errors.Is and errors.As can inspect either cause through the multi-error unwrap.
func (*RecoveryError) Error ¶ added in v0.9.0
func (e *RecoveryError) Error() string
func (*RecoveryError) Unwrap ¶ added in v0.9.0
func (e *RecoveryError) Unwrap() []error
type RetryOptions ¶ added in v0.9.0
type RetryOptions struct {
MaxAttempts int
Backoff time.Duration
MaxBackoff time.Duration
Idempotent bool
}
RetryOptions enables bounded retries for an explicitly idempotent action. MaxAttempts includes the first run; zero disables retries. Backoff doubles between attempts and is capped by MaxBackoff.
type Scope ¶ added in v0.6.0
type Scope struct {
// contains filtered or unexported fields
}
Scope owns a module's lifetime. Close cancels Context, stops observations, then runs deferred drains in reverse registration order. Register stores first, persistence second, and actions last so work finishes before persistence drains and stores close. Shared stores need not belong to a Scope.
func (*Scope) Close ¶ added in v0.6.0
Close starts shutdown once. Concurrent callers share its result but can stop waiting with their own context. The first caller supplies the drain context; resources receive that deadline even if the caller stops waiting. Persistence that must drain should use an independent worker context, not Scope.Context, which is canceled first.
type SelectorOptions ¶
SelectorOptions configures a selector subscription. With no Equals function, comparable values use == (including pointer identity); other values use reflect.DeepEqual. A custom comparator can use slices.Equal or maps.Equal.
type Snapshot ¶
Snapshot is a source value accepted at Version. TransactionID is non-zero when the publication belongs to Atomic2 or AtomicGroup; derived sources propagate it so one transaction can be settled before downstream delivery.
func Select ¶ added in v0.6.0
Select reads an owned projection and source version without cloning the whole Store. project borrows read-only state under the read lock: it must not mutate or retain source references, reenter the Store, or block. Return scalars or copy all mutable references in the projection (for example with slices.Clone). Existing Get and SubscribeSelector keep their full-state isolation policy.
type Storage ¶
type Storage interface {
Load(context.Context) ([]byte, error)
Save(context.Context, []byte) error
}
Storage is the minimal persistence backend used by Persistence. Files, key-value databases and encrypted stores can implement it.
type Store ¶
type Store[T any] struct { // contains filtered or unexported fields }
func NewWithClone ¶ added in v0.3.0
NewWithClone isolates owned state, snapshots, subscriber values and rollback with clone. Clone must copy all mutable references, be safe for concurrent calls, and must not reenter the store. A nil clone preserves New's behavior.
func (*Store[T]) Action ¶
Action returns a reusable named action function, matching Pinia action definitions while keeping Go's explicit function types.
func (*Store[T]) ActiveTasks ¶ added in v0.6.0
func (s *Store[T]) ActiveTasks() []ActionTrace
ActiveTasks returns queued/running tasks independently of diagnostic sampling. The slice is owned by the caller.
func (*Store[T]) Batch ¶
Batch groups notifications from multiple updates into one final snapshot. It does not roll back successful updates when fn returns an error; use Atomic2 or AtomicGroup when the updates must commit as one transaction.
func (*Store[T]) BatchErr ¶ added in v0.9.1
BatchErr groups notifications like Batch and returns the callback's error. Updates completed before an error remain committed, but their subscribers are still notified once when the outermost batch ends.
func (*Store[T]) SubscribeActionHooks ¶
func (s *Store[T]) SubscribeActionHooks(hooks ActionHooks) func()
SubscribeActionHooks registers lifecycle hooks and returns an unsubscribe function.
func (*Store[T]) SubscribeActionTrace ¶ added in v0.6.0
func (s *Store[T]) SubscribeActionTrace(fn func(ActionTrace)) func()
func (*Store[T]) SubscribeActions ¶
func (s *Store[T]) SubscribeActions(fn func(ActionEvent)) func()
func (*Store[T]) SubscribeMutations ¶
func (*Store[T]) SubscribeSnapshot ¶ added in v0.7.0
func (*Store[T]) SubscribeWith ¶
func (s *Store[T]) SubscribeWith(options SubscribeOptions[T], fn func(Snapshot[T])) func()
func (*Store[T]) TaskSnapshot ¶ added in v0.6.0
func (s *Store[T]) TaskSnapshot() TaskSnapshot
func (*Store[T]) Transaction ¶
func (*Store[T]) Update ¶
Example ¶
package main
import (
"fmt"
"github.com/yldm-tech/nodra"
)
func main() {
store := nodra.New(0)
store.Subscribe(func(snapshot nodra.Snapshot[int]) {
fmt.Println(snapshot.Value, snapshot.Version)
})
if err := store.Update(func(value *int) { *value = 42 }); err != nil {
fmt.Println(err)
}
}
Output: 42 1
type SubscribeOptions ¶
type Task ¶ added in v0.6.0
type Task struct {
ID uint64
// contains filtered or unexported fields
}
Task is one run of an AsyncAction. Cancel requests cooperative cancellation; Wait waits for work and its completion notifications, independently of the waiter's context. Do not Wait from the task's own work, hooks or synchronous subscribers.
func (*Task) Status ¶ added in v0.6.0
func (t *Task) Status() ReadOnly[AsyncState]
type TaskMetrics ¶ added in v0.7.0
type TaskMetrics struct {
Attempts int `json:"attempts"`
Debounce time.Duration `json:"debounce"`
Queue time.Duration `json:"queue"`
Work time.Duration `json:"work"`
Commit time.Duration `json:"commit"`
QueueDepth int `json:"queueDepth"`
ActiveWorkers int `json:"activeWorkers"`
}
TaskMetrics separates elapsed debounce, eligible queue wait, user work and commit attempt. Commit includes Store lock wait, cloning, apply and synchronous mutation notifications, even on failure. Work excludes Before hooks. These phases exclude other hooks/status callbacks and need not sum to Duration. QueueDepth counts live waiting tasks (including debounce); ActiveWorkers includes canceled work until it returns. Counts are sampled per event for this AsyncAction, not globally.
type TaskSnapshot ¶ added in v0.6.0
type TaskSnapshot struct {
Revision uint64
Tasks []ActionTrace
}
TaskSnapshot identifies current queued/running tasks and the source trace revision at capture time. Terminal tasks are released by the source; Task.Status retains their final state.
type VersionedJSONCodec ¶ added in v0.3.0
type VersionedJSONCodec[T any] struct { Version int Migrations map[int]JSONMigration AllowUnversioned bool }
VersionedJSONCodec stores {"schemaVersion": N, "state": ...}. Migrations[n] upgrades version n to n+1, sequentially. AllowUnversioned accepts legacy raw JSON as version zero. Version must be positive; future versions are rejected. Decode never writes storage: persist a successful upgrade explicitly with Save.
func (VersionedJSONCodec[T]) Decode ¶ added in v0.3.0
func (c VersionedJSONCodec[T]) Decode(data []byte) (T, error)
func (VersionedJSONCodec[T]) Encode ¶ added in v0.3.0
func (c VersionedJSONCodec[T]) Encode(value T) ([]byte, error)
Source Files
¶
Directories
¶
| Path | Synopsis |
|---|---|
|
Package devtools provides an opt-in local inspector for Nodra stores.
|
Package devtools provides an opt-in local inspector for Nodra stores. |
|
examples
|
|
|
basic
command
|
|
|
devtools
command
Command devtools runs a counter with a live local inspector.
|
Command devtools runs a counter with a live local inspector. |
|
mygo
command
Example mygo demonstrates two native windows sharing business state with independent redraw subscriptions.
|
Example mygo demonstrates two native windows sharing business state with independent redraw subscriptions. |
|
persistent
command
Command persistent demonstrates cloned state, schema migration and write-behind persistence.
|
Command persistent demonstrates cloned state, schema migration and write-behind persistence. |
|
workbench
command
Command workbench searches real local file names and edits persisted settings.
|
Command workbench searches real local file names and edits persisted settings. |
