Help us shape the future of Go! Take the 2026 Go Developer Survey

nodra

package module
v0.2.0 Latest Latest
Warning

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

Go to latest
Published: Oct 21, 2026 License: MIT Imports: 9 Imported by: 0

README

nodra

Nodra is a production-oriented reactive state store for Go desktop applications. It is a state library for MyGo apps with Zustand-like ergonomics, and does not tie your state to a renderer or a persistence backend.

Install

go get github.com/yldm-tech/nodra@latest

Everything is in the root package, github.com/yldm-tech/nodra.

MyGo usage

Redraw a native MyGo window whenever its store publishes:

stop := nodra.Invalidate(store, window)

Call stop when the window closes; go run ./examples/mygo shows the whole app.

Core usage

import nodra "github.com/yldm-tech/nodra"

tasks := nodra.New([]Task{})
stop := tasks.SubscribeWith(nodra.SubscribeOptions[[]Task]{Immediate: true}, func(snapshot nodra.Snapshot[[]Task]) {
	log.Printf("%d tasks", len(snapshot.Value))
})
defer stop()

tasks.Update(func(value *[]Task) {
	*value = append(*value, task)
})

What is included

  • Generic, concurrency-safe Store[T] with versioned snapshots
  • Set, Update, and UpdateErr with rollback on returned errors and panics
  • Subscribe and SubscribeWith with immediate delivery and per-subscriber equality
  • SubscribeSelector and SubscribeSelectorWith for focused updates with previous values
  • Derive for memoized computed/getter values
  • Action / Do with action timing and error events
  • SubscribeMutations with direct, patch, reset and action metadata
  • Create, Define, State, Patch, and Reset aliases for familiar Pinia/Zustand workflows
  • Store IDs and independent named store instances
  • Action lifecycle hooks: before, after, and error
  • Nested Batch notification groups that publish one final snapshot
  • Transaction for one serialized, fallible mutation
  • Track history with bounded undo and redo
  • Persistence[T] with pluggable storage and codec interfaces
  • Ready-to-use FileStorage and JSONCodec[T] with atomic file replacement
  • AutoSaveWithErrors for observable persistence failures
  • Context-bound, bounded Watch streams that close with the store
  • Invalidate to redraw a MyGo window when a store publishes

Design guarantees

  • Store locks are released before user listeners run, so listeners can safely read or update the same store.
  • Versions increment once per published update, not once per mutation inside a batch.
  • Closing a store is permanent; updates return nodra.ErrClosed and active watches terminate.
  • Values are not deep copied. Use immutable value conventions or copy slices and maps at the update boundary.
  • Watch channels are best effort and bounded; slow consumers may skip intermediate snapshots.

Repository layout

*.go                           store, history, persistence, MyGo invalidation, and tests (package nodra)
examples/                      standalone and native MyGo examples
docs/architecture.md           lifecycle and threading details

The repository is one Go module, github.com/yldm-tech/nodra, with a single go.mod at the root.

Verification

go test -race ./...
go vet ./...

Run the native MyGo example with go run ./examples/mygo.

CI also runs golangci-lint, cross builds for Linux and Windows, and checks that the module path is github.com/yldm-tech/nodra, that there is no other go.mod, go.work or replace directive, and that go mod tidy leaves no diff.

Releases

The module is released by release-please with plain vX.Y.Z tags and one root CHANGELOG.md.

  1. Every push to main updates the release PR when there are releasable commits since the last tag. Merging that PR tags the release and publishes a GitHub release.
  2. After a release, post-release.yml checks that the tag resolves through proxy.golang.org.

Commit messages follow Conventional Commits:

  • feat: ... adds a feature (minor bump)
  • fix: ... fixes a bug (patch bump)
  • feat!: ... or a BREAKING CHANGE: footer marks a breaking change (minor bump while the version is below 1.0.0)
  • docs, test, refactor, ci, build and chore do not trigger a release

Use a scope where it helps, e.g. fix(persist): ....

Release PRs are pushed with the workflow's GITHUB_TOKEN. GitHub holds their test runs at "approval required", so the release workflow approves those runs itself (.github/scripts/start-pr-ci.sh). Setting a RELEASE_PLEASE_TOKEN repository secret (a fine-grained PAT or GitHub App token with contents and pull request write access) makes them trigger CI directly instead.

Pinia/Zustand-style example

store := nodra.Create(Counter{Count: 0})
increment := store.Action("increment", func(state *Counter) error {
    state.Count++
    return nil
})
double := nodra.Derive(store, func(state Counter) int {
    return state.Count * 2
}, func(a, b int) bool { return a == b })

_ = increment()
fmt.Println(store.State().Count, double.Get())

See docs/architecture.md for the full concept mapping.

Named stores

counter := nodra.Define("counter", func() Counter {
    return Counter{Count: 0}
})
store := counter.New()

Each call to New creates an independent instance with the same initial-state factory. The ID is available to mutation and action observers for logging and devtools integrations.

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

Examples

Constants

This section is empty.

Variables

View Source
var (
	ErrNoUndo = errors.New("nodra: nothing to undo")
	ErrNoRedo = errors.New("nodra: nothing to redo")
)

ErrNoUndo and ErrNoRedo report an empty history stack.

View Source
var ErrClosed = errors.New("nodra: store is closed")
View Source
var ErrInvalidPersistence = errors.New("nodra: invalid persistence configuration")

Functions

func Invalidate added in v0.2.0

func Invalidate[T any](store *Store[T], window *mygo.Window) (stop func())

Invalidate requests a new frame of window's native Content whenever store publishes a snapshot, so a MyGo view that reads the store during its render shows the new state. It returns the function that stops the subscription; with a nil store or window it subscribes to nothing and returns a no-op.

func SubscribeSelector

func SubscribeSelector[T any, S comparable](store *Store[T], selector func(T) S, fn func(S)) func()

func SubscribeSelectorWith

func SubscribeSelectorWith[T any, V comparable](store *Store[T], selector func(T) V, fn func(current, previous V), options SelectorOptions[V]) func()

SubscribeSelectorWith calls fn with current and previous selected values.

Types

type ActionEvent

type ActionEvent struct {
	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 Codec

type Codec[T any] interface {
	Encode(T) ([]byte, error)
	Decode([]byte) (T, error)
}

Codec serializes and restores one state value.

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 (*Definition[T]) ID

func (d *Definition[T]) ID() string

func (*Definition[T]) New

func (d *Definition[T]) New() *Store[T]

type Derived

type Derived[T any, V any] struct {
	// contains filtered or unexported fields
}

Derived is a memoized getter derived from a store. It recomputes when the source publishes and only notifies listeners when the selected value changes.

func Derive

func Derive[T any, V any](store *Store[T], selector func(T) V, equals func(V, V) bool) *Derived[T, V]

func (*Derived[T, V]) Close

func (d *Derived[T, V]) Close() error

func (*Derived[T, V]) Get

func (d *Derived[T, V]) Get() V

func (*Derived[T, V]) Subscribe

func (d *Derived[T, V]) Subscribe(fn func(V)) func()

type FileStorage

type FileStorage struct {
	Path string
	Perm os.FileMode
}

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) Load

func (f FileStorage) Load(ctx context.Context) ([]byte, error)

func (FileStorage) Save

func (f FileStorage) Save(ctx context.Context, data []byte) (err error)

type History

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

History records successful store snapshots and provides bounded undo/redo.

func Track

func Track[T any](store *Store[T], limit int) *History[T]

Track starts recording changes to store. limit is the maximum number of undo entries; values below one disable history recording.

func (*History[T]) CanRedo

func (h *History[T]) CanRedo() bool

func (*History[T]) CanUndo

func (h *History[T]) CanUndo() bool

func (*History[T]) Close

func (h *History[T]) Close() error

Close stops recording future changes. It does not close the underlying store.

func (*History[T]) Redo

func (h *History[T]) Redo() error

Redo restores the next state.

func (*History[T]) Undo

func (h *History[T]) Undo() error

Undo restores the previous state.

type JSONCodec

type JSONCodec[T any] struct{}

JSONCodec encodes a store value using encoding/json.

func (JSONCodec[T]) Decode

func (JSONCodec[T]) Decode(data []byte) (T, error)

func (JSONCodec[T]) Encode

func (JSONCodec[T]) Encode(value T) ([]byte, error)

type Mutation

type Mutation[T any] struct {
	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"
)

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.

func NewPersistence

func NewPersistence[T any](store *Store[T], storage Storage, codec Codec[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]) 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.

func (*Persistence[T]) Load

func (p *Persistence[T]) Load(ctx context.Context) error

func (*Persistence[T]) Save

func (p *Persistence[T]) Save(ctx context.Context) error

type SelectorOptions

type SelectorOptions[V comparable] struct {
	Immediate bool
	Equals    func(previous, next V) bool
}

SelectorOptions configures a selector subscription.

type Snapshot

type Snapshot[T any] struct {
	Value   T
	Version uint64
}

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 Create

func Create[T any](initial T) *Store[T]

Create is the Pinia/Zustand-style constructor name for a typed store.

func New

func New[T any](initial T) *Store[T]

func (*Store[T]) Action

func (s *Store[T]) Action(name string, fn func(*T) error) func() error

Action returns a reusable named action function, matching Pinia action definitions while keeping Go's explicit function types.

func (*Store[T]) Batch

func (s *Store[T]) Batch(fn func()) error

func (*Store[T]) Close

func (s *Store[T]) Close() error

func (*Store[T]) Do

func (s *Store[T]) Do(name string, fn func(*T) error) error

Do runs a named mutation and emits an action event after it completes.

func (*Store[T]) Get

func (s *Store[T]) Get() T

func (*Store[T]) ID

func (s *Store[T]) ID() string

func (*Store[T]) Patch

func (s *Store[T]) Patch(fn func(*T)) error

func (*Store[T]) Reset

func (s *Store[T]) Reset() error

func (*Store[T]) Set

func (s *Store[T]) Set(value T) error

func (*Store[T]) Snapshot

func (s *Store[T]) Snapshot() Snapshot[T]

func (*Store[T]) State

func (s *Store[T]) State() T

func (*Store[T]) Subscribe

func (s *Store[T]) Subscribe(fn func(Snapshot[T])) func()

func (*Store[T]) SubscribeActionHooks

func (s *Store[T]) SubscribeActionHooks(hooks ActionHooks) func()

SubscribeActionHooks registers lifecycle hooks and returns an unsubscribe function.

func (*Store[T]) SubscribeActions

func (s *Store[T]) SubscribeActions(fn func(ActionEvent)) func()

func (*Store[T]) SubscribeMutations

func (s *Store[T]) SubscribeMutations(fn func(Mutation[T])) func()

func (*Store[T]) SubscribeWith

func (s *Store[T]) SubscribeWith(options SubscribeOptions[T], fn func(Snapshot[T])) func()

func (*Store[T]) Transaction

func (s *Store[T]) Transaction(fn func(*T) error) error

func (*Store[T]) Update

func (s *Store[T]) Update(fn func(*T)) error
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

func (*Store[T]) UpdateErr

func (s *Store[T]) UpdateErr(fn func(*T) error) error

func (*Store[T]) Version

func (s *Store[T]) Version() uint64

func (*Store[T]) Watch

func (s *Store[T]) Watch(ctx context.Context, buffer int) <-chan Snapshot[T]

type SubscribeOptions

type SubscribeOptions[T any] struct {
	Immediate bool
	Equals    func(previous, next T) bool
}

Directories

Path Synopsis
examples
basic command
mygo command
Example mygo demonstrates Nodra driving a native MyGo view.
Example mygo demonstrates Nodra driving a native MyGo view.

Jump to

Keyboard shortcuts

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