Documentation
¶
Overview ¶
Package reco is an experimental research prototype for reactive computation.
EXPERIMENTAL: DO NOT USE ¶
Nothing to see here; move along. This is unfinished research, not a supported library. Do not depend on it. APIs and behavior may change or disappear without notice. There are no compatibility or correctness guarantees.
Overview ¶
Reco stands for reactive computation. It provides typed, in-process reactive dataflow graphs. Declare mutable leaves with Data, SetData, MultisetData, MapData, or StructData, and derived nodes with Func. Register an output with Graph.Register to register its dependencies too. Graph.Update applies a transaction, recomputes affected nodes in dependency order, and delivers subscription events after the graph has settled. Node definitions can be reused across independent Graph instances. Scope and In instantiate the same definitions independently within one graph; explicitly scoped inputs allow dependencies across scopes.
Each definition has a NodeClassName, supplied to its constructor and returned by Node.ClassName. Class names must be unique within a graph/scope, including its dependencies; they are distinct from collection keys and future instance keys.
Incremental operators ¶
Package github.com/bradfitz/reco/nodes provides reusable incremental nodes, including set union and set-to-map computation. Packages can implement their own operators using Operator and Input. Allocate incremental caches inside the operator's per-instance compute factory. Consume collection changes with ChangesSince and publish one WithDelta batch per evaluation to preserve the incremental fast path. See the Operator example. Eval.ChangedInputs identifies changed dependency handles without scanning all declared inputs. The first evaluation reports all inputs; later evaluations report only value/result-status changes. It retains original declaration handles across scoping. Input reads the current settled value in O(1) expected time. Readiness and scope mapping do not rebuild input tables on point edits. Func still populates every field of its dependency struct; use Operator for wide, incrementally maintained computations. See the ChangedInputs example.
Published values must be immutable. SetSnapshot, MultisetSnapshot and MapSnapshot provide structurally shared storage, native Go iterators, and atomic delta application. Custom immutable types may implement ValueEqualer and VersionedValue. Multisets store positive int64 multiplicities, with signed atomic adjustments and checked underflow/overflow. Len counts distinct keys; All yields key/count pairs. nodes.CountBy and nodes.GroupCounts aggregate contributors, and nodes.Distinct suppresses count changes that do not cross zero. Indexes and counted classes can avoid expanding relationships into every node pair.
Demand-driven lifetimes ¶
NewGraph keeps registered functions eager. NewGraphWithOptions with GraphOptions.DemandDriven enabled computes only subscribed dependency closures. Unsubscribe releases now-unused derived values and operator caches; ordinary authoritative leaves and registered definitions remain. Read temporarily activates a closure, returning an immutable snapshot without a lasting watch. SubscribeSnapshot, SubscribeMap, and SubscribeStruct atomically snapshot and subscribe to an output, so changes cannot fall between reading and watching. Reconnecting rebuilds released caches from current leaves. Stats exposes constant-time lifetime and evaluation counters. Reconfigure changes a function instance's inputs and compute factory inside a transaction while retaining its handle and subscribers. It requires a demand-driven graph and already-registered, lower-rank inputs. Newly needed inputs load before commit; abort releases temporary demand. Shared definitions in other graph instances remain unchanged.
BindDurable optionally binds a registered data node to an application-supplied loader. Its value is cached only while observed, and reloaded when demand returns. This works for scalars, sets, multisets, maps, records, and custom immutable values. Read/Subscribe return load errors without retaining partial demand. Set and collection mutation helpers reject these externally owned leaves. Commit backing storage inside Graph.Update, then use UpdateDurable to patch the resident cache; cold publications need neither loading nor computation. ReadCached probes residency without loading. Storage itself is application code: reco neither writes a database nor undoes an external commit. Loaders run synchronously under the graph lock and must not reenter it. Returned snapshots and loader closures can retain values independently; avoid capturing a full initial snapshot in a loader intended to save memory.
Typed records ¶
StructSnapshot[T] uses an ordinary Go struct as a fixed, typed schema. Field handles provide typed edits and change inspection. SubscribeStruct atomically obtains an initial snapshot and watches settled field/collection changes. StructChanges.Then coalesces contiguous events without scanning unchanged collection contents. Use MapFieldChanges and SetFieldChanges on the composed batch rather than comparing its endpoint collections; MultisetFieldChanges provides the same touched-key behavior for counted fields. See the SubscribeStruct example for an incremental stream-encoding boundary.
Experimental remote watches ¶
Package github.com/bradfitz/reco/metagraph connects explicit remote mirrors and authoritative exports using multiplexed bidirectional WebSockets. The core Graph remains local and synchronous. Remote-watch ownership and lifetime are currently application-managed, not automatically resolved through a Locator. See cmd/webdemo for a two-process example and the current limitations.
Index ¶
- Variables
- func ApplyMultisetDelta[K comparable](tx *Tx, node Node[MultisetSnapshot[K]], d MultisetDelta[K]) error
- func ApplySetDelta[K comparable](tx *Tx, node Node[SetSnapshot[K]], d SetDelta[K])
- func ApplyStructDelta[T any](tx *Tx, node Node[StructSnapshot[T]], d StructDelta[T])
- func BindDurable[T any](g *Graph, node Node[T], load func() (T, error)) error
- func Deps[T any](v T) inlineDeps
- func MapDelete[K comparable, V any](tx *Tx, node Node[MapSnapshot[K, V]], key K)
- func MapFieldChanges[T any, K comparable, V any](field StructField[T, MapSnapshot[K, V]], c StructChanges[T]) iter.Seq[MapChange[K, V]]
- func MapPut[K comparable, V any](tx *Tx, node Node[MapSnapshot[K, V]], key K, value V)
- func MultisetFieldChanges[T any, K comparable](field StructField[T, MultisetSnapshot[K]], c StructChanges[T]) iter.Seq[MultisetChange[K]]
- func Reconfigure[T any](tx *Tx, node Node[T], deps []Dependency, factory func() Compute[T]) (err error)
- func Set[T any](tx *Tx, node Node[T], value T)
- func SetDelete[K comparable](tx *Tx, node Node[SetSnapshot[K]], key K)
- func SetFieldChanges[T any, K comparable](field StructField[T, SetSnapshot[K]], c StructChanges[T]) iter.Seq[SetChange[K]]
- func SetUpsert[K comparable](tx *Tx, node Node[SetSnapshot[K]], key K)
- func SubscribeMap[K comparable, V any](g *Graph, node Node[MapSnapshot[K, V]], opts SubscribeOptions, ...) (Snapshot[MapSnapshot[K, V]], SubscriptionHandle, error)
- func SubscribeSnapshot[T any](g *Graph, node Node[T], opts SubscribeOptions, fn func(Event[T])) (Snapshot[T], SubscriptionHandle, error)
- func SubscribeStruct[T any](g *Graph, node Node[StructSnapshot[T]], opts SubscribeOptions, ...) (Snapshot[StructSnapshot[T]], SubscriptionHandle, error)
- func UpdateDurable[T any](tx *Tx, node Node[T], update func(T) T)
- type Compute
- type Dep
- type Dependency
- type Eval
- type Event
- type Graph
- type GraphOptions
- type GraphStats
- type MapChange
- type MapDelta
- type MapEntry
- type MapEvent
- type MapSnapshot
- func (s MapSnapshot[K, V]) All() iter.Seq2[K, V]
- func (s MapSnapshot[K, V]) Changes() []MapChange[K, V]
- func (s MapSnapshot[K, V]) ChangesSince(previous MapSnapshot[K, V]) iter.Seq[MapChange[K, V]]
- func (s MapSnapshot[K, V]) Get(k K) (V, bool)
- func (s MapSnapshot[K, V]) Len() int
- func (s MapSnapshot[K, V]) Range(fn func(K, V) bool)
- func (s MapSnapshot[K, V]) RecoValueEqual(other any) bool
- func (s MapSnapshot[K, V]) RecoWithVersion(version Version) any
- func (s MapSnapshot[K, V]) Version() Version
- func (s MapSnapshot[K, V]) WithDelta(d MapDelta[K, V]) MapSnapshot[K, V]
- type MultisetChange
- type MultisetDelta
- type MultisetEntry
- type MultisetSnapshot
- func (s MultisetSnapshot[K]) All() iter.Seq2[K, int64]
- func (s MultisetSnapshot[K]) Changes() []MultisetChange[K]
- func (s MultisetSnapshot[K]) ChangesSince(previous MultisetSnapshot[K]) iter.Seq[MultisetChange[K]]
- func (s MultisetSnapshot[K]) Count(k K) int64
- func (s MultisetSnapshot[K]) Counts() MapSnapshot[K, int64]
- func (s MultisetSnapshot[K]) Len() int
- func (s MultisetSnapshot[K]) Range(fn func(K, int64) bool)
- func (s MultisetSnapshot[K]) RecoValueEqual(other any) bool
- func (s MultisetSnapshot[K]) RecoWithVersion(v Version) any
- func (s MultisetSnapshot[K]) Version() Version
- func (s MultisetSnapshot[K]) WithDelta(d MultisetDelta[K]) (MultisetSnapshot[K], error)
- type Node
- func Data[T any](className NodeClassName) Node[T]
- func Func[DepsT any, Out any](className NodeClassName, deps any, compute func(Eval, DepsT) Result[Out]) Node[Out]
- func In[T any](scope *Scope, node Node[T]) Node[T]
- func MapData[K comparable, V any](className NodeClassName) Node[MapSnapshot[K, V]]
- func MultisetData[K comparable](name NodeClassName) Node[MultisetSnapshot[K]]
- func Operator[T any](className NodeClassName, deps []Dependency, newCompute func() Compute[T]) Node[T]
- func SetData[K comparable](className NodeClassName) Node[SetSnapshot[K]]
- func StructData[T any](className NodeClassName) Node[StructSnapshot[T]]
- type NodeClassName
- type Result
- type Scope
- type SetChange
- type SetDelta
- type SetSnapshot
- func (s SetSnapshot[K]) All() iter.Seq[K]
- func (s SetSnapshot[K]) Changes() []SetChange[K]
- func (s SetSnapshot[K]) ChangesSince(previous SetSnapshot[K]) iter.Seq[SetChange[K]]
- func (s SetSnapshot[K]) Contains(k K) bool
- func (s SetSnapshot[K]) Len() int
- func (s SetSnapshot[K]) Range(fn func(K) bool)
- func (s SetSnapshot[K]) RecoValueEqual(other any) bool
- func (s SetSnapshot[K]) RecoWithVersion(version Version) any
- func (s SetSnapshot[K]) Version() Version
- func (s SetSnapshot[K]) WithDelta(d SetDelta[K]) SetSnapshot[K]
- type Snapshot
- type StructChanges
- func (c StructChanges[T]) After() StructSnapshot[T]
- func (c StructChanges[T]) Before() StructSnapshot[T]
- func (c StructChanges[T]) ChangeCount() int
- func (c StructChanges[T]) Fields() iter.Seq[string]
- func (c StructChanges[T]) Len() int
- func (c StructChanges[T]) Then(next StructChanges[T]) (StructChanges[T], error)
- type StructDelta
- type StructEdit
- type StructEvent
- type StructField
- type StructSnapshot
- func (s StructSnapshot[T]) ChangesSince(previous StructSnapshot[T]) StructChanges[T]
- func (s StructSnapshot[T]) RecoValueEqual(other any) bool
- func (s StructSnapshot[T]) RecoWithVersion(v Version) any
- func (s StructSnapshot[T]) Value() (value T)
- func (s StructSnapshot[T]) Version() Version
- func (s StructSnapshot[T]) WithDelta(d StructDelta[T]) StructSnapshot[T]
- type SubscribeOptions
- type SubscriptionHandle
- type Tx
- type ValueEqualer
- type Version
- type VersionedValue
Examples ¶
Constants ¶
This section is empty.
Variables ¶
var ErrMultisetCount = errors.New("reco: multiset count out of range")
ErrMultisetCount reports a negative count or an int64 count overflow.
Functions ¶
func ApplyMultisetDelta ¶
func ApplyMultisetDelta[K comparable](tx *Tx, node Node[MultisetSnapshot[K]], d MultisetDelta[K]) error
ApplyMultisetDelta applies one validated batch to the staged value. On error it leaves this call's staged value unchanged; return the error from Update to abort the entire transaction. Each call must leave valid counts, even if a subsequent call would undo it. Multiple calls retain the transaction's original delta base and normalize net-zero changes at commit.
func ApplySetDelta ¶
func ApplySetDelta[K comparable](tx *Tx, node Node[SetSnapshot[K]], d SetDelta[K])
ApplySetDelta applies d inside tx. Clear enumerates existing members so the prototype's point-change subscribers also receive every removal.
func ApplyStructDelta ¶
func ApplyStructDelta[T any](tx *Tx, node Node[StructSnapshot[T]], d StructDelta[T])
ApplyStructDelta applies an atomic field batch inside tx. Multiple calls compose against staged values and retain the transaction's original base.
func BindDurable ¶
BindDurable makes a registered data node a cache of externally stored data. It requires a demand-driven graph and a node with no observers. Any current value is discarded. The binding belongs to this graph and scoped instance, not the reusable node definition.
load returns an immutable value and runs synchronously under the graph lock. It must not reenter the graph or acquire a lock held by a graph caller. Errors are returned by Read/Subscribe; failed activation releases all acquired demand and can be retried. The cached value is released with its last observer. Definitions, the loader, and snapshots retained by callers remain alive.
Set and collection mutation helpers reject bound nodes. To change one, commit its backing store and call UpdateDurable within the same Graph.Update callback. Only that callback may write the backing store while the graph is in use. This serializes reloads with durable commits and cache publication.
Example ¶
g := NewGraphWithOptions(GraphOptions{DemandDriven: true})
n := Data[string]("greeting")
g.Register(n)
stored := "hello" // Stand-in for a database record.
BindDurable(g, n, func() (string, error) { return stored, nil })
g.Update(func(tx *Tx) error {
stored = "good morning" // Commit storage first; return errors before publication.
UpdateDurable(tx, n, func(string) string { return stored })
return nil
})
snapshot, _ := Read(g, n)
fmt.Println(snapshot.Value(), g.Stats().CachedDurableNodes)
Output: good morning 0
func MapDelete ¶
func MapDelete[K comparable, V any](tx *Tx, node Node[MapSnapshot[K, V]], key K)
MapDelete removes one map entry.
func MapFieldChanges ¶
func MapFieldChanges[T any, K comparable, V any](field StructField[T, MapSnapshot[K, V]], c StructChanges[T]) iter.Seq[MapChange[K, V]]
MapFieldChanges yields a map field's net entry changes, even when c combines many events. It never re-diffs the endpoints. Entries have explicit before/ after presence bits, preserving nil and zero values. Order is unspecified.
func MapPut ¶
func MapPut[K comparable, V any](tx *Tx, node Node[MapSnapshot[K, V]], key K, value V)
MapPut upserts one map entry.
func MultisetFieldChanges ¶
func MultisetFieldChanges[T any, K comparable](field StructField[T, MultisetSnapshot[K]], c StructChanges[T]) iter.Seq[MultisetChange[K]]
MultisetFieldChanges yields a multiset field's net count changes, including after StructChanges.Then coalescing. It visits only changed keys, never re-diffing the endpoint collections. Zero counts denote absence.
func Reconfigure ¶
func Reconfigure[T any](tx *Tx, node Node[T], deps []Dependency, factory func() Compute[T]) (err error)
Reconfigure stages new inputs and a compute factory for a function instance, without replacing its handle or subscriptions. It requires a demand-driven graph. Inputs must already be registered at lower topological ranks than node; this restriction preserves ordering and rules out cycles without a graph scan. The reusable definition and other graph instances are not changed.
An active function acquires its new inputs now, returning loader errors before commit. The old inputs/output remain in use until the transaction succeeds. Aborting releases temporary demand. Commit replaces the operator cache and computes the new output atomically with other writes. An inactive function only changes its binding, without loading data. Configure each instance at most once per transaction. If committing external storage, call this first. Any returned error also aborts the transaction, even if the caller ignores it. Active bindings require initialized inputs (or inputs already staged for writing/reconfiguration in this transaction), so an uninitialized replacement cannot silently leave subscribers seeing the old computation's value.
func Set ¶
Set assigns a data node within a transaction. For externally owned leaves bound with BindDurable, use UpdateDurable; Set makes the transaction fail.
func SetDelete ¶
func SetDelete[K comparable](tx *Tx, node Node[SetSnapshot[K]], key K)
SetDelete removes one set key.
func SetFieldChanges ¶
func SetFieldChanges[T any, K comparable](field StructField[T, SetSnapshot[K]], c StructChanges[T]) iter.Seq[SetChange[K]]
SetFieldChanges yields a set field's net membership changes, even when c combines many events. It never re-diffs the endpoints. Order is unspecified.
func SetUpsert ¶
func SetUpsert[K comparable](tx *Tx, node Node[SetSnapshot[K]], key K)
SetUpsert adds one set key.
func SubscribeMap ¶
func SubscribeMap[K comparable, V any](g *Graph, node Node[MapSnapshot[K, V]], opts SubscribeOptions, fn func(MapEvent[K, V])) (Snapshot[MapSnapshot[K, V]], SubscriptionHandle, error)
SubscribeMap atomically returns the current snapshot and subscribes fn to later transaction-local map mutations. The atomic setup prevents updates from being lost between taking the initial snapshot and starting the watch.
func SubscribeSnapshot ¶
func SubscribeSnapshot[T any](g *Graph, node Node[T], opts SubscribeOptions, fn func(Event[T])) (Snapshot[T], SubscriptionHandle, error)
SubscribeSnapshot atomically reads node and installs a watch for subsequent changes. Unlike a separate Read followed by Subscribe, no update can fall in between. The callback may run as soon as this function releases the graph lock, including before the caller has processed the returned snapshot. Callbacks run under that lock and must not reenter the graph or do network I/O.
func SubscribeStruct ¶
func SubscribeStruct[T any](g *Graph, node Node[StructSnapshot[T]], opts SubscribeOptions, fn func(StructEvent[T])) (Snapshot[StructSnapshot[T]], SubscriptionHandle, error)
SubscribeStruct atomically returns the current snapshot and watches later changes. An invalid initial snapshot means the node has not produced a value; use the first event as the initial state in that case. Events may start before this function returns, so initialize callback state before calling it.
Callbacks run synchronously under the graph lock after propagation. Capture or accumulate the immutable changes there; encode/compress/write outside the callback. Changes.Then supports coalescing without scanning full snapshots, but callers must bound pending state and decide when to disconnect/resync a slow consumer. Options have the same current limitations as Subscribe. Snapshot/Event still do not expose Result error/partial status; validity alone does not establish application-level completeness or readiness.
Example ¶
package main
import (
"fmt"
"slices"
"github.com/bradfitz/reco"
"github.com/bradfitz/reco/nodes"
)
func main() {
// A miniature control-server meta record. The actual application can use
// NodeID/PeerInfo and add DNS, filter, policy, and other typed fields.
type meta struct {
Domain string
Peers reco.MapSnapshot[int, string]
}
domain := reco.Data[string]("domain")
peers := reco.MapData[int, string]("peers")
mrm := nodes.Struct[meta]("mapResponseMeta", struct {
Domain reco.Node[string]
Peers reco.Node[reco.MapSnapshot[int, string]]
}{domain, peers})
domainField := reco.Field[meta, string]("Domain")
peerField := reco.Field[meta, reco.MapSnapshot[int, string]]("Peers")
g := reco.NewGraph()
if err := g.Register(mrm); err != nil {
panic(err)
}
update := func(fn func(*reco.Tx)) {
if err := g.Update(func(tx *reco.Tx) error { fn(tx); return nil }); err != nil {
panic(err)
}
}
update(func(tx *reco.Tx) {
reco.Set(tx, domain, "example.net")
reco.MapPut(tx, peers, 1, "original")
reco.MapPut(tx, peers, 2, "leaving")
})
// Callbacks only accumulate immutable semantic changes. A real stream
// writer would synchronize access to pending and bound its retained size.
// Serialization, compression, and network writes happen outside callbacks.
var pending reco.StructChanges[meta]
initial, sub, err := reco.SubscribeStruct(g, mrm, reco.SubscribeOptions{}, func(ev reco.StructEvent[meta]) {
var err error
pending, err = pending.Then(ev.Changes)
if err != nil {
panic(err)
} // A production stream would terminate/resync.
})
if err != nil {
panic(err)
}
defer sub.Unsubscribe()
fmt.Println("initial:", initial.Value().Value().Domain, initial.Value().Value().Peers.Len(), "peers")
// Updates can arrive while the initial snapshot is being sent.
update(func(tx *reco.Tx) { reco.MapPut(tx, peers, 1, "intermediate"); reco.MapPut(tx, peers, 3, "joined") })
update(func(tx *reco.Tx) {
reco.MapPut(tx, peers, 1, "final")
reco.MapDelete(tx, peers, 2)
reco.Set(tx, domain, "new.example.net")
})
if _, current, changed := domainField.Change(pending); changed {
fmt.Println("domain:", current)
}
// Sort only the changed peers, not the full live map. Even though the
// writer coalesced events, no deep endpoint-snapshot comparison is needed.
changes := slices.Collect(reco.MapFieldChanges(peerField, pending))
slices.SortFunc(changes, func(a, b reco.MapChange[int, string]) int { return a.Key - b.Key })
for _, change := range changes {
if change.AfterValid {
fmt.Println("upsert:", change.Key, change.After)
} else {
fmt.Println("remove:", change.Key)
}
}
}
Output: initial: example.net 2 peers domain: new.example.net upsert: 1 final remove: 2 upsert: 3 joined
func UpdateDurable ¶
UpdateDurable publishes an already-persisted mutation to a durable leaf's cache. If cold, it does nothing, including not calling update: a later observer reloads the latest stored value. If resident, update receives the current immutable value (including earlier publications in this transaction). Return a delta-based snapshot to preserve incremental downstream computation.
Commit the backing store inside the same Graph.Update callback, before calling UpdateDurable, and return its error without publishing if the commit fails. Do all fallible preparation before committing; graph rollback cannot undo an external commit. This function never writes storage itself.
Types ¶
type Compute ¶
Compute evaluates an operator using its declared inputs. Its closure may retain instance-local incremental caches, but must not mutate published values. The result must depend only on the inputs, not on the cache's history.
type Dep ¶
Dep is the typed value wrapper used by typed-struct dependency inputs.
func Input ¶
Input returns the current value, version, and result status of a declared dependency during a computation. It panics for a zero or undeclared node or outside an evaluation. It does not acquire a graph lock or create a watch. Unlike Changes(), collection ChangesSince(previous) safely handles both unchanged inputs and replacements since the operator last evaluated them.
type Dependency ¶
type Dependency interface {
// contains filtered or unexported methods
}
Dependency is a node that an operator declares as an input. Every Node[T] implements Dependency, allowing heterogeneous or variable-length input lists. Create nodes with Data, Func, or Operator; the runtime owns their handles.
type Eval ¶
type Eval struct {
// contains filtered or unexported fields
}
Eval represents one synchronous node evaluation. Use Input to read its declared dependencies; ChangedInputs identifies which dependencies changed. It is not a cancellation or deadline context. Do not retain an Eval after the computation returns.
func (Eval) ChangedInputs ¶
func (e Eval) ChangedInputs() iter.Seq[Dependency]
ChangedInputs yields declared input handles whose value or result status changed since this computation's previous invocation. The first invocation of a compute instance yields all inputs, including after demand eviction or Reconfigure. Errors returned by Compute do not defer this boundary: retain unapplied work if recovery needs changes from earlier failed evaluations.
Handles are Node[T] values of the declared types, so callers can type assert or look them up in a map keyed by Dependency. Scoping preserves the handles from the original declaration, just as Input does. Repeated declarations of the same node yield it once; distinct declared handles that resolve to the same scoped instance are each yielded. Order is unspecified. All values read with Input are settled for this transaction, whether changed or not.
The iterator is reusable within this evaluation, supports early termination, and visits only changed handles. Do not retain it or Eval after computation returns. It panics outside an evaluation. Func can use it too, but populating Func's dependency struct still visits all of that struct's fields.
Example ¶
package main
import (
"fmt"
"github.com/bradfitz/reco"
)
func main() {
a, b, c := reco.Data[int]("a"), reco.Data[int]("b"), reco.Data[int]("c")
sum := reco.Operator("sum", []reco.Dependency{a, b, c}, func() reco.Compute[int] {
previous := make(map[reco.Node[int]]int)
total := 0
return func(e reco.Eval) reco.Result[int] {
// All three inputs on first evaluation; afterwards only the inputs
// that changed. Duplicate declarations would be reported once.
for input := range e.ChangedInputs() {
n := input.(reco.Node[int]) // This operator declares only int inputs.
current := reco.Input(e, n).Value()
total += current - previous[n]
previous[n] = current
}
return reco.OK(total)
}
})
g := reco.NewGraph()
if err := g.Register(sum); err != nil {
panic(err)
}
if err := g.Update(func(tx *reco.Tx) error {
reco.Set(tx, a, 10)
reco.Set(tx, b, 20)
reco.Set(tx, c, 30)
return nil
}); err != nil {
panic(err)
}
value, _ := reco.Read(g, sum)
fmt.Println(value.Value())
if err := g.Update(func(tx *reco.Tx) error { reco.Set(tx, b, 25); return nil }); err != nil {
panic(err)
}
value, _ = reco.Read(g, sum)
fmt.Println(value.Value())
}
Output: 60 65
type Graph ¶
type Graph struct {
// contains filtered or unexported fields
}
Graph is an independent reactive dataflow graph instance.
func NewGraph ¶
func NewGraph() *Graph
NewGraph creates an empty, eager graph instance. See NewGraphWithOptions for demand-driven evaluation and automatic cache release.
func NewGraphWithOptions ¶
func NewGraphWithOptions(opts GraphOptions) *Graph
NewGraphWithOptions creates a graph with the specified evaluation lifetime.
func (*Graph) Register ¶
Register atomically adds definitions and dependencies, validating class name uniqueness within each Scope and DAG structure. Work is proportional to newly registered definitions and their edges, not the existing graph's size.
func (*Graph) Stats ¶
func (g *Graph) Stats() GraphStats
Stats returns a constant-time snapshot of graph counters.
type GraphOptions ¶
type GraphOptions struct {
// DemandDriven evaluates only functions reachable from subscriptions.
// Read evaluates on demand without retaining a watch. When the last
// observer goes away, derived values and operator caches are released.
// Registered definitions and ordinary data values remain available.
// BindDurable permits reloadable data values to be released too.
DemandDriven bool
}
GraphOptions configures evaluation lifetime.
type GraphStats ¶
type GraphStats struct {
Nodes, Functions, ActiveFunctions, CachedFunctions, Subscriptions int
DurableNodes, CachedDurableNodes int
Evaluations uint64
// InputChecks counts dependency readiness checks, including first-demand
// initialization. InputReads counts values requested by computations (also
// including Func's struct adapter). They measure work, not elapsed time.
InputChecks, InputReads uint64
}
GraphStats reports graph lifetime and work counters. Registered definitions and ordinary data leaves outlive subscriptions. CachedFunctions counts retained operator states. DurableNodes counts loader bindings and CachedDurableNodes counts their resident values. Demand-driven graphs release both types of cache with last demand, without unregistering definitions.
type MapChange ¶
type MapChange[K comparable, V any] struct { Key K Before V BeforeValid bool After V AfterValid bool }
MapChange describes one key changed by a map transaction. Before and After are accompanied by validity bits so zero values remain unambiguous.
type MapDelta ¶
type MapDelta[K comparable, V any] struct { Clear bool Remove []K Put []MapEntry[K, V] }
MapDelta describes one atomic clear, remove, then put batch. Put wins over removal; repeated Put keys use the last entry in the slice. A nil value is distinct from removal. Clear plus Put replaces the entire map.
type MapEntry ¶
type MapEntry[K comparable, V any] struct { Key K Value V }
MapEntry is a whole key/value assignment, including a nil value if V allows it.
type MapEvent ¶
type MapEvent[K comparable, V any] struct { Previous MapSnapshot[K, V] Current MapSnapshot[K, V] Changes []MapChange[K, V] Version Version }
MapEvent is delivered after a transaction changes a map node. Changes lists only changed keys. Consecutive snapshots use their recorded delta; arbitrary snapshot replacements are reconciled against the previous value.
type MapSnapshot ¶
type MapSnapshot[K comparable, V any] struct { // contains filtered or unexported fields }
MapSnapshot is a stable, structurally shared snapshot of a map node value. Point updates copy only the HAMT path to the changed key. The zero value is an empty map; use WithDelta to construct or update one. Entry values use ValueEqualer when implemented, otherwise Go equality for comparable values and reflect.DeepEqual for other values. Pointer values are compared by identity, not by the contents of the objects they point to.
func (MapSnapshot[K, V]) All ¶
func (s MapSnapshot[K, V]) All() iter.Seq2[K, V]
All returns an iterator over the snapshot's key/value pairs in unspecified order. It visits entries lazily without copying the backing map and stops when the range loop breaks. Each iteration starts afresh over this snapshot, even if the graph has since changed. A zero snapshot yields no entries.
Example ¶
package main
import (
"fmt"
"slices"
"github.com/bradfitz/reco"
)
func main() {
lengths := reco.MapData[string, int]("lengths")
g := reco.NewGraph()
if err := g.Register(lengths); err != nil {
panic(err)
}
if err := g.Update(func(tx *reco.Tx) error {
reco.MapPut(tx, lengths, "cedar", 5)
reco.MapPut(tx, lengths, "oak", 3)
return nil
}); err != nil {
panic(err)
}
snapshot, err := reco.Read(g, lengths)
if err != nil {
panic(err)
}
var entries []string
for word, length := range snapshot.Value().All() {
entries = append(entries, fmt.Sprintf("%s:%d", word, length))
}
slices.Sort(entries) // Iteration order is unspecified.
fmt.Println(entries)
}
Output: [cedar:5 oak:3]
func (MapSnapshot[K, V]) Changes ¶
func (s MapSnapshot[K, V]) Changes() []MapChange[K, V]
Changes returns a copy of the net point mutations that produced this snapshot. It is not relative to an arbitrary reader's previous snapshot; use ChangesSince when consuming updates, including unchanged inputs and snapshot replacements.
func (MapSnapshot[K, V]) ChangesSince ¶
func (s MapSnapshot[K, V]) ChangesSince(previous MapSnapshot[K, V]) iter.Seq[MapChange[K, V]]
ChangesSince iterates net entry changes from previous to s. It uses a recorded delta when its base matches previous, otherwise compares the snapshots (O(N)). Identical roots yield nothing. Each key appears at most once, in unspecified order, with whole before/after values and explicit presence bits.
func (MapSnapshot[K, V]) Get ¶
func (s MapSnapshot[K, V]) Get(k K) (V, bool)
Get returns a value by key.
func (MapSnapshot[K, V]) Len ¶
func (s MapSnapshot[K, V]) Len() int
Len returns the number of map entries.
func (MapSnapshot[K, V]) Range ¶
func (s MapSnapshot[K, V]) Range(fn func(K, V) bool)
Range calls fn for each entry in unspecified order until fn returns false.
func (MapSnapshot[K, V]) RecoValueEqual ¶
func (s MapSnapshot[K, V]) RecoValueEqual(other any) bool
RecoValueEqual implements ValueEqualer using persistent-root identity.
func (MapSnapshot[K, V]) RecoWithVersion ¶
func (s MapSnapshot[K, V]) RecoWithVersion(version Version) any
RecoWithVersion implements VersionedValue without mutating this snapshot.
func (MapSnapshot[K, V]) Version ¶
func (s MapSnapshot[K, V]) Version() Version
Version returns the graph-local version at which this snapshot was observed.
func (MapSnapshot[K, V]) WithDelta ¶
func (s MapSnapshot[K, V]) WithDelta(d MapDelta[K, V]) MapSnapshot[K, V]
WithDelta returns a new snapshot after applying d, without modifying s. It preserves HAMT structural sharing, normalizes repeated touched keys, and returns s for a net-zero change. Changed snapshots have Version zero until observed by a graph. Values must be treated as immutable by callers. Prefer one batch per operator evaluation; see SetSnapshot.WithDelta.
Example ¶
package main
import (
"fmt"
"github.com/bradfitz/reco"
)
func main() {
var names reco.MapSnapshot[int, string] // An empty map, no graph required.
names = names.WithDelta(reco.MapDelta[int, string]{
Put: []reco.MapEntry[int, string]{{Key: 1, Value: "old"}},
})
replacement := names.WithDelta(reco.MapDelta[int, string]{
Clear: true,
Put: []reco.MapEntry[int, string]{{Key: 2, Value: "new"}},
})
fmt.Println(names.Get(1))
fmt.Println(replacement.Get(2))
}
Output: old true new true
type MultisetChange ¶
type MultisetChange[K comparable] struct { Key K Before, After int64 }
MultisetChange is a net multiplicity change. Zero denotes absence. Before and After are nonnegative; After-Before is the signed adjustment.
type MultisetDelta ¶
type MultisetDelta[K comparable] struct { Clear bool Remove []K Put []MultisetEntry[K] Adjust map[K]int64 }
MultisetDelta is an atomic clear, remove, put, then adjust batch. Remove deletes all occurrences. Repeated Put keys use the last assignment. Adjust adds one signed net change per key; it does not assign a count. Every Put count and every final adjusted count must be in [0, math.MaxInt64]. Clear plus Put replaces the multiset. Input slices/maps are not retained.
Additive adjustments are not idempotent: replaying +1 adds another occurrence. Remote delivery still needs base/version checks; this is not a CRDT counter.
type MultisetEntry ¶
type MultisetEntry[K comparable] struct { Key K Count int64 }
MultisetEntry assigns a key's multiplicity. Count must be nonnegative; zero removes the key. Counts are per key, not a bounded collection total.
type MultisetSnapshot ¶
type MultisetSnapshot[K comparable] struct { // contains filtered or unexported fields }
MultisetSnapshot is an immutable counted set, backed by a persistent map. The zero value is empty. Only positive counts are stored; removing one occurrence does not remove a key until its last occurrence disappears. Keys use Go equality and must be reflexive (no NaNs), as for SetSnapshot. Interface keys must be dynamically comparable.
Example ¶
package main
import (
"fmt"
"github.com/bradfitz/reco"
)
func main() {
var counts reco.MultisetSnapshot[string]
counts, err := counts.WithDelta(reco.MultisetDelta[string]{
Put: []reco.MultisetEntry[string]{{Key: "web", Count: 2}},
})
if err != nil {
panic(err)
}
counts, err = counts.WithDelta(reco.MultisetDelta[string]{Adjust: map[string]int64{"web": -1}})
if err != nil {
panic(err)
}
fmt.Println(counts.Len(), counts.Count("web"))
// Decrementing below zero fails atomically; the previous value is returned.
unchanged, err := counts.WithDelta(reco.MultisetDelta[string]{Adjust: map[string]int64{"web": -2}})
fmt.Println(err != nil, unchanged.Count("web"))
}
Output: 1 1 true 1
func (MultisetSnapshot[K]) All ¶
func (s MultisetSnapshot[K]) All() iter.Seq2[K, int64]
All lazily iterates distinct key/count pairs, as Range does.
func (MultisetSnapshot[K]) Changes ¶
func (s MultisetSnapshot[K]) Changes() []MultisetChange[K]
Changes returns a copy of this snapshot's last net count changes, not changes relative to an arbitrary reader. No-op edits retain old metadata, so prefer ChangesSince when consuming updates.
func (MultisetSnapshot[K]) ChangesSince ¶
func (s MultisetSnapshot[K]) ChangesSince(previous MultisetSnapshot[K]) iter.Seq[MultisetChange[K]]
ChangesSince yields one net change per touched key in unspecified order. Identical roots yield nothing, adjacent deltas take O(delta) work plus map traversal, and skipped bases/replacements may require O(N) reconciliation.
func (MultisetSnapshot[K]) Count ¶
func (s MultisetSnapshot[K]) Count(k K) int64
Count returns k's multiplicity, or zero if absent.
func (MultisetSnapshot[K]) Counts ¶
func (s MultisetSnapshot[K]) Counts() MapSnapshot[K, int64]
Counts returns an immutable map view without copying. Updating the returned map does not modify the multiset. There is no unchecked map-to-multiset cast.
func (MultisetSnapshot[K]) Len ¶
func (s MultisetSnapshot[K]) Len() int
Len returns the number of DISTINCT keys, not the sum of their counts.
func (MultisetSnapshot[K]) Range ¶
func (s MultisetSnapshot[K]) Range(fn func(K, int64) bool)
Range visits each distinct key and its positive count in unspecified order, stopping when fn returns false. It does not repeat a key Count times.
func (MultisetSnapshot[K]) RecoValueEqual ¶
func (s MultisetSnapshot[K]) RecoValueEqual(other any) bool
RecoValueEqual implements ValueEqualer using persistent-root identity.
func (MultisetSnapshot[K]) RecoWithVersion ¶
func (s MultisetSnapshot[K]) RecoWithVersion(v Version) any
RecoWithVersion implements VersionedValue without modifying this snapshot.
func (MultisetSnapshot[K]) Version ¶
func (s MultisetSnapshot[K]) Version() Version
Version returns the graph-local observation version.
func (MultisetSnapshot[K]) WithDelta ¶
func (s MultisetSnapshot[K]) WithDelta(d MultisetDelta[K]) (MultisetSnapshot[K], error)
WithDelta applies d atomically. An invalid count returns s and an error wrapping ErrMultisetCount, without applying ANY part of d. Underflow is an error, not a clamped deletion. Net-zero updates reuse the original root. Point edits do touched-key work plus HAMT path copying; Clear enumerates existing keys to report removals. Changed snapshots have Version zero.
type Node ¶
type Node[T any] struct { // contains filtered or unexported fields }
Node is a typed handle to a value in a graph.
func Func ¶
func Func[DepsT any, Out any](className NodeClassName, deps any, compute func(Eval, DepsT) Result[Out]) Node[Out]
Func creates a function node using either a typed binding struct or an inline dependency declaration created with Deps.
func In ¶
In returns the instance of node in scope, recursively instantiating its unscoped dependencies. Already-scoped nodes are preserved, allowing explicit dependencies between scopes in the same graph. ClassName is unchanged. In is safe to call concurrently. Nil scope and zero node handles panic.
Example ¶
package main
import (
"fmt"
"github.com/bradfitz/reco"
)
func main() {
input := reco.Data[int]("input")
doubled := reco.Func("doubled", reco.Deps(struct{ N reco.Node[int] }{input}),
func(_ reco.Eval, in struct{ N int }) reco.Result[int] {
return reco.OK(2 * in.N)
})
var left, right reco.Scope
a, b := reco.In(&left, doubled), reco.In(&right, doubled)
g := reco.NewGraphWithOptions(reco.GraphOptions{DemandDriven: true})
if err := g.Register(a, b); err != nil {
panic(err)
}
if err := g.Update(func(tx *reco.Tx) error {
reco.Set(tx, reco.In(&left, input), 3)
reco.Set(tx, reco.In(&right, input), 7)
return nil
}); err != nil {
panic(err)
}
// A one-shot read computes current state without keeping it active.
av, err := reco.Read(g, a)
if err != nil {
panic(err)
}
bv, err := reco.Read(g, b)
if err != nil {
panic(err)
}
fmt.Println(av.Value(), bv.Value())
fmt.Println("active functions:", g.Stats().ActiveFunctions)
}
Output: 6 14 active functions: 0
func MapData ¶
func MapData[K comparable, V any](className NodeClassName) Node[MapSnapshot[K, V]]
MapData creates a mutable map data node.
func MultisetData ¶
func MultisetData[K comparable](name NodeClassName) Node[MultisetSnapshot[K]]
MultisetData declares a mutable counted-set leaf. Use ApplyMultisetDelta or Set with a validated snapshot. Ordinary SubscribeSnapshot observes its changes.
func Operator ¶
func Operator[T any](className NodeClassName, deps []Dependency, newCompute func() Compute[T]) Node[T]
Operator declares a reusable incremental node. newCompute is called lazily, per graph/node instance, to create its compute function and state. In demand- driven graphs, the state is discarded when last demand disappears and the factory runs again on reactivation. Caches must be rebuildable from inputs. Allocate mutable caches INSIDE newCompute, not in the shared definition. A factory must return a non-nil function. It can run concurrently with factories/computations in other graphs; calls within one graph are serialized.
Compute reads declared inputs with Input. The runtime waits until they are initialized, schedules affected nodes in dependency order, and delivers the settled results. A compute function must not call Read, Update, or Subscribe on its graph, perform external side effects, or retain its Eval.
For collections, use ChangesSince to consume input deltas and WithDelta to publish a new immutable output. A normal stateless computation can use Func. For many inputs, Eval.ChangedInputs avoids inspecting unchanged dependencies. It reports changes since the previous evaluation, even if that evaluation returned an error; operators must retain any unapplied work needed to recover.
Example ¶
package main
import (
"fmt"
"strings"
"github.com/bradfitz/reco"
)
func main() {
words := reco.SetData[string]("words")
// This is an external-package implementation of a key-preserving map.
uppercase := reco.Operator("uppercase", []reco.Dependency{words}, func() reco.Compute[reco.MapSnapshot[string, string]] {
// These caches belong to one graph, not to the reusable definition.
var previous reco.SetSnapshot[string]
var result reco.MapSnapshot[string, string]
return func(eval reco.Eval) reco.Result[reco.MapSnapshot[string, string]] {
current := reco.Input(eval, words).Value()
var delta reco.MapDelta[string, string]
for c := range current.ChangesSince(previous) {
if c.Present {
// Only newly added words are converted.
delta.Put = append(delta.Put, reco.MapEntry[string, string]{Key: c.Key, Value: strings.ToUpper(c.Key)})
} else {
delta.Remove = append(delta.Remove, c.Key)
}
}
previous = current
result = result.WithDelta(delta) // One atomic, structurally shared batch.
return reco.OK(result)
}
})
g := reco.NewGraph()
if err := g.Register(uppercase); err != nil {
panic(err)
}
if err := g.Update(func(tx *reco.Tx) error {
reco.ApplySetDelta(tx, words, reco.SetDelta[string]{Add: []string{"oak", "elm"}})
return nil
}); err != nil {
panic(err)
}
first, err := reco.Read(g, uppercase)
if err != nil {
panic(err)
}
if err := g.Update(func(tx *reco.Tx) error {
reco.ApplySetDelta(tx, words, reco.SetDelta[string]{Remove: []string{"elm"}, Add: []string{"pine"}})
return nil
}); err != nil {
panic(err)
}
current, err := reco.Read(g, uppercase)
if err != nil {
panic(err)
}
fmt.Println(current.Value().Get("oak"))
fmt.Println(current.Value().Get("pine"))
fmt.Println(first.Value().Get("elm")) // Previously published values stay intact.
}
Output: OAK true PINE true ELM true
func SetData ¶
func SetData[K comparable](className NodeClassName) Node[SetSnapshot[K]]
SetData creates a mutable set data node.
func StructData ¶
func StructData[T any](className NodeClassName) Node[StructSnapshot[T]]
StructData declares a mutable record data node and validates T's schema.
func (Node[T]) ClassName ¶
func (n Node[T]) ClassName() NodeClassName
ClassName returns the name of the node's definition, or "" for a zero handle.
type NodeClassName ¶
type NodeClassName string
NodeClassName names a reusable node definition (a node class). Constructors such as Data, Func, and Operator take a class name; Node.ClassName returns it.
A name must be nonempty. Names are exact, case-sensitive strings: reco does not trim whitespace, normalize text, or interpret separators or punctuation. There is no required namespace or path syntax. The zero value is invalid for a definition, but is returned by ClassName on a zero Node handle.
Within a Graph and Scope, all definitions, including dependencies, must have distinct names regardless of their value types or constructors. Unscoped definitions share the graph's default namespace. Registering the same definition again is allowed; separately constructed definitions with the same name are an error, not aliases. Definitions and names can be reused across independent graphs or instantiated with In in multiple scopes; names are not globally unique.
A class name is not a collection key or a key selecting an instance of a class. Scope provides local instances; generic keyed resolution and remote addressing are not yet implemented; this type does not define a distributed address or versioning scheme.
type Result ¶
Result is the value produced by a function node.
A partial result is still a value and may be propagated. Err is deliberately part of Result rather than a second return value so compute functions keep all failure state explicit in the dataflow model.
type Scope ¶
type Scope struct {
// contains filtered or unexported fields
}
Scope gives reusable definitions independent instances within one Graph. It is a local namespace, not a separate graph, transaction boundary, or security boundary. A Scope must not be copied after first use. Its zero value is ready for use; retain it to obtain the same instances on subsequent calls.
type SetChange ¶
type SetChange[K comparable] struct { Key K Present bool }
SetChange describes one key changed by a set transaction.
type SetDelta ¶
type SetDelta[K comparable] struct { Clear bool Remove []K Add []K }
SetDelta is an atomic clear, remove, then add batch. Add wins when a key is also removed. The enclosing transaction may batch changes to other nodes.
type SetSnapshot ¶
type SetSnapshot[K comparable] struct { // contains filtered or unexported fields }
SetSnapshot is a stable, structurally shared snapshot of a set node value. The zero value is an empty set; use WithDelta to construct or update one.
func (SetSnapshot[K]) All ¶
func (s SetSnapshot[K]) All() iter.Seq[K]
All returns an iterator over the snapshot's keys in unspecified order. It visits keys lazily without copying the backing set and stops when the range loop breaks. Each iteration starts afresh over this snapshot, even if the graph has since changed. A zero snapshot yields no keys.
Example ¶
package main
import (
"fmt"
"slices"
"github.com/bradfitz/reco"
)
func main() {
words := reco.SetData[string]("words")
g := reco.NewGraph()
if err := g.Register(words); err != nil {
panic(err)
}
if err := g.Update(func(tx *reco.Tx) error {
reco.ApplySetDelta(tx, words, reco.SetDelta[string]{Add: []string{"cedar", "birch"}})
return nil
}); err != nil {
panic(err)
}
snapshot, err := reco.Read(g, words)
if err != nil {
panic(err)
}
var names []string
for word := range snapshot.Value().All() {
names = append(names, word)
}
slices.Sort(names) // Iteration order is unspecified.
fmt.Println(names)
}
Output: [birch cedar]
func (SetSnapshot[K]) Changes ¶
func (s SetSnapshot[K]) Changes() []SetChange[K]
Changes returns a copy of the net point mutations that produced this snapshot. It is not relative to an arbitrary reader's previous snapshot; use ChangesSince when consuming updates, including unchanged inputs and snapshot replacements.
func (SetSnapshot[K]) ChangesSince ¶
func (s SetSnapshot[K]) ChangesSince(previous SetSnapshot[K]) iter.Seq[SetChange[K]]
ChangesSince iterates net membership changes from previous to s. Consecutive snapshots use their recorded delta in O(delta) work; identical backing roots yield nothing. Initial snapshots, skipped versions, and arbitrary replacements fall back to comparing memberships, which can take O(N). Versions alone are not used to establish lineage, since snapshots can come from different graphs. Each key is yielded at most once, in unspecified order.
func (SetSnapshot[K]) Contains ¶
func (s SetSnapshot[K]) Contains(k K) bool
Contains reports whether k is present.
func (SetSnapshot[K]) Len ¶
func (s SetSnapshot[K]) Len() int
Len returns the number of set entries.
func (SetSnapshot[K]) Range ¶
func (s SetSnapshot[K]) Range(fn func(K) bool)
Range calls fn for each key in unspecified order until fn returns false.
func (SetSnapshot[K]) RecoValueEqual ¶
func (s SetSnapshot[K]) RecoValueEqual(other any) bool
RecoValueEqual implements ValueEqualer using persistent-root identity.
func (SetSnapshot[K]) RecoWithVersion ¶
func (s SetSnapshot[K]) RecoWithVersion(version Version) any
RecoWithVersion implements VersionedValue without mutating this snapshot.
func (SetSnapshot[K]) Version ¶
func (s SetSnapshot[K]) Version() Version
Version returns the graph-local version at which this snapshot was observed.
func (SetSnapshot[K]) WithDelta ¶
func (s SetSnapshot[K]) WithDelta(d SetDelta[K]) SetSnapshot[K]
WithDelta returns a new snapshot after applying d, without modifying s or any published value. Point edits copy only their immutable HAMT paths. Clear enumerates removals for the point-change event API. Net-zero edits return s. A changed result has Version zero until observed by a graph, and its Changes describe the net changes from s. A no-op returns s, including its old Changes; use ChangesSince(s) to distinguish this case. Prefer one batch per operator evaluation to preserve the O(delta) path through ChangesSince and SubscribeMap.
type Snapshot ¶
type Snapshot[T any] struct { // contains filtered or unexported fields }
Snapshot is the immutable view delivered through subscription events.
For scalar values, Value returns the value itself. Collection snapshots can use richer concrete types while still carrying the same graph version.
func Read ¶
Read returns the latest value of a registered node. In demand-driven mode it temporarily observes its dependency closure and releases that demand before returning; the returned immutable snapshot remains valid.
func ReadCached ¶
ReadCached returns a node's resident snapshot without loading, computing, or adding demand. An invalid snapshot means the node is cold or uninitialized. It is useful for point reads backed by a store lookup when a collection is cold. Returned snapshots retain their values independently of graph demand.
type StructChanges ¶
type StructChanges[T any] struct { // contains filtered or unexported fields }
StructChanges is an immutable net change batch between two record snapshots. It retains only its endpoints and touched-key changes, not an event history. A zero value is an empty accumulator. Then composes contiguous batches without re-diffing collection snapshots; external synchronization is needed only when assigning a shared accumulator, not for reading a published batch.
func (StructChanges[T]) After ¶
func (c StructChanges[T]) After() StructSnapshot[T]
After returns the final snapshot, retaining the composed changes from Before.
func (StructChanges[T]) Before ¶
func (c StructChanges[T]) Before() StructSnapshot[T]
Before returns the initial snapshot of the batch.
func (StructChanges[T]) ChangeCount ¶
func (c StructChanges[T]) ChangeCount() int
ChangeCount counts scalar field replacements and map/set entry changes. It inspects only the fixed field list, not pending collection entries. This can help bound a stream accumulator; it is not a byte-size or memory bound.
func (StructChanges[T]) Fields ¶
func (c StructChanges[T]) Fields() iter.Seq[string]
Fields yields changed field names in schema order. It inspects only the fixed field list, never collection contents. The iterator is reusable.
func (StructChanges[T]) Len ¶
func (c StructChanges[T]) Len() int
Len returns the number of fields with nonempty net changes.
func (StructChanges[T]) Then ¶
func (c StructChanges[T]) Then(next StructChanges[T]) (StructChanges[T], error)
Then composes a subsequent batch. It rejects a gap or out-of-order batch: next.Before must match this batch's actual After root and observed version, not a reconstructed equal value. Versions alone cannot establish lineage across graphs. A zero accumulator accepts any first batch. Work is proportional to the fixed field count and incoming touched keys plus persistent-map path copying.
type StructDelta ¶
type StructDelta[T any] []StructEdit[T]
StructDelta is an ordered atomic batch of field edits. Repeated edits see earlier values; the last Set wins. An empty batch does nothing. Fields cannot be added or deleted. Edits and referenced values must not be mutated while the batch is being applied.
type StructEdit ¶
type StructEdit[T any] struct { // contains filtered or unexported fields }
StructEdit is an opaque typed field edit, created by StructField.Set or Update. Its zero value is invalid.
type StructEvent ¶
type StructEvent[T any] struct { Previous StructSnapshot[T] Current StructSnapshot[T] Changes StructChanges[T] Version Version }
StructEvent describes one settled record change. Changes includes typed field replacements and net map/set mutations. Copying the event is cheap and safe; all published snapshots and change batches are immutable.
type StructField ¶
type StructField[T, V any] struct { // contains filtered or unexported fields }
StructField is a typed handle to a declared field of T. Its zero value is invalid. Create one with Field, normally once alongside node definitions.
func Field ¶
func Field[T, V any](name string) StructField[T, V]
Field finds an exported, non-embedded field by its Go name. It panics if T is not a supported struct, the field is missing, or its type is not exactly V. Validation happens at declaration time; edits and reads remain typed.
func (StructField[T, V]) Change ¶
func (f StructField[T, V]) Change(c StructChanges[T]) (before, after V, changed bool)
Change returns this field's before/after values and whether it changed. When unchanged, both returned values are zero. For collection entry changes, use MapFieldChanges or SetFieldChanges instead of diffing these snapshots.
func (StructField[T, V]) Get ¶
func (f StructField[T, V]) Get(s StructSnapshot[T]) V
Get returns the field's typed value.
func (StructField[T, V]) Name ¶
func (f StructField[T, V]) Name() string
Name returns the field's Go name.
func (StructField[T, V]) Set ¶
func (f StructField[T, V]) Set(value V) StructEdit[T]
Set replaces this field, including with a nil, zero, false, or empty value.
func (StructField[T, V]) Update ¶
func (f StructField[T, V]) Update(fn func(V) V) StructEdit[T]
Update transforms this field, seeing earlier edits in the same atomic delta. fn must be pure and must not mutate its input. For collection fields, return current.WithDelta(batch) to preserve delta-sized work.
type StructSnapshot ¶
type StructSnapshot[T any] struct { // contains filtered or unexported fields }
StructSnapshot is an immutable record whose schema is the Go struct T. T must be a struct with exported, non-embedded fields. Value returns a typed shallow copy; referenced objects must remain immutable, as with other reco values. A zero snapshot contains the zero value of T.
Fields use ValueEqualer when available, otherwise ordinary scalar equality. MapSnapshot and SetSnapshot fields preserve their incremental changes. Other fields are whole-value replacements, not recursively patched Go objects. Record overhead is proportional to the fixed field count; scalar comparison still follows each value's equality contract. Unchanged map/set contents are never scanned. Use Field to declare typed edits and inspect changes, and SubscribeStruct to obtain an initial snapshot plus future changes.
func NewStruct ¶
func NewStruct[T any](value T) StructSnapshot[T]
NewStruct wraps value without copying its referenced storage. It validates the schema but does not enumerate collection contents or calculate changes.
func (StructSnapshot[T]) ChangesSince ¶
func (s StructSnapshot[T]) ChangesSince(previous StructSnapshot[T]) StructChanges[T]
ChangesSince describes the net field/entry changes from previous to s. Adjacent retained collection deltas avoid scanning unchanged contents. If collection versions were skipped or replaced without lineage, reconciliation may enumerate them. Capture each event and use StructChanges.Then to coalesce efficiently, rather than discarding intermediate snapshots and diffing later.
func (StructSnapshot[T]) RecoValueEqual ¶
func (s StructSnapshot[T]) RecoValueEqual(other any) bool
RecoValueEqual compares field values, respecting their ValueEqualer hooks. Collection fields compare persistent roots, not backing-store contents.
func (StructSnapshot[T]) RecoWithVersion ¶
func (s StructSnapshot[T]) RecoWithVersion(v Version) any
RecoWithVersion implements VersionedValue without changing field versions.
func (StructSnapshot[T]) Value ¶
func (s StructSnapshot[T]) Value() (value T)
Value returns the typed record. Do not mutate objects referenced by it.
func (StructSnapshot[T]) Version ¶
func (s StructSnapshot[T]) Version() Version
Version returns the graph-local version at which this record was observed.
func (StructSnapshot[T]) WithDelta ¶
func (s StructSnapshot[T]) WithDelta(d StructDelta[T]) StructSnapshot[T]
WithDelta applies all edits without publishing intermediate states. It coalesces nested collection changes, preserving their original delta base. Net-zero edits reuse the previous storage. Whole collection replacements without retained delta lineage may require enumeration; point edits do not.
Example ¶
package main
import (
"fmt"
"github.com/bradfitz/reco"
)
func main() {
type settings struct {
Enabled bool
Names reco.SetSnapshot[string]
}
enabled := reco.Field[settings, bool]("Enabled")
names := reco.Field[settings, reco.SetSnapshot[string]]("Names")
before := reco.NewStruct(settings{Enabled: true})
after := before.WithDelta(reco.StructDelta[settings]{
enabled.Set(false), // Explicit false, not an omitted change.
names.Update(func(s reco.SetSnapshot[string]) reco.SetSnapshot[string] {
return s.WithDelta(reco.SetDelta[string]{Add: []string{"elm"}})
}),
})
changes := after.ChangesSince(before)
if old, value, changed := enabled.Change(changes); changed {
fmt.Println("enabled:", old, "->", value)
}
for change := range reco.SetFieldChanges(names, changes) {
fmt.Println("member:", change.Key, change.Present)
}
fmt.Println("old snapshot:", before.Value().Enabled, before.Value().Names.Len())
}
Output: enabled: true -> false member: elm true old snapshot: true 0
type SubscribeOptions ¶
SubscribeOptions controls subscription delivery.
type SubscriptionHandle ¶
type SubscriptionHandle interface {
Unsubscribe() error
}
SubscriptionHandle cancels a subscription.
func Subscribe ¶
func Subscribe[T any](g *Graph, node Node[T], opts SubscribeOptions, fn func(Event[T])) (SubscriptionHandle, error)
Subscribe registers a callback for node value changes, without an initial event. In demand-driven mode it keeps the dependency closure active until Unsubscribe. Callbacks run under the graph lock and must not reenter it.
type Tx ¶
type Tx struct {
// contains filtered or unexported fields
}
Tx is a serialized per-graph transaction.
type ValueEqualer ¶
ValueEqualer optionally replaces reflect.DeepEqual for a custom value type. RecoValueEqual must be a pure equivalence test, handle a different concrete type by returning false, and compare semantic values rather than versions or delta metadata. Immutable values can compare persistent-root identities. Implement this on the published value type (not only on its pointer type if the graph stores values). Scalar values without this hook use reflect.DeepEqual; map entries use Go equality when comparable, otherwise reflect.DeepEqual.
type VersionedValue ¶
VersionedValue optionally stamps an observed graph version into a custom value. RecoWithVersion must return the SAME concrete type and must not mutate the receiver or previously published snapshots. This hook is independent of ValueEqualer; most custom values need neither hook. Snapshot.Version remains available even when the value does not implement VersionedValue.
Source Files
¶
Directories
¶
| Path | Synopsis |
|---|---|
|
cmd
|
|
|
webdemo
command
Command webdemo runs an in-memory reco playground with optional peer watches.
|
Command webdemo runs an in-memory reco playground with optional peer watches. |
|
Package metagraph is an EXPERIMENTAL, unstable prototype of multiplexed cross-process reco watches.
|
Package metagraph is an EXPERIMENTAL, unstable prototype of multiplexed cross-process reco watches. |
|
Package nodes provides reusable incremental node definitions for reco graphs.
|
Package nodes provides reusable incremental node definitions for reco graphs. |
|
Package recotestcontrol is an experimental, reco-backed Tailscale test control server.
|
Package recotestcontrol is an experimental, reco-backed Tailscale test control server. |