Files
mesh-sdk/go/process/pernode.go
T

116 lines
3.8 KiB
Go

package process
import (
"context"
"encoding/json"
"strings"
"github.com/nats-io/nats.go/jetstream"
stdio "git.novox.be/novox/mesh-sdk/go"
"git.novox.be/novox/mesh-sdk/go/pernode"
)
// A state its owner keeps one bucket per node (novox/hq ADR 0593 §6: `composed-<node>`), read as one state on this
// connection: the rules are pernode's, this connection only adapts to its Bus, opening a bucket as State does (a bucket
// not granted refused promptly, naming it).
// What a reader of a per-node state is granted beside each bucket's read (pernode.ListSubject, CreatedAdvisory).
const (
FamilyListSubject = pernode.ListSubject
FamilyCreatedAdvisory = pernode.CreatedAdvisory
)
// NodeFamily is a per-node state read across every node's bucket on this connection.
type NodeFamily struct {
Read string
Prefix string
c *Conn
}
// ReadPerNode is another module's state kept one bucket per node, named read in the reader's manifest, its buckets
// prefix followed by the node's name.
func (c *Conn) ReadPerNode(read, prefix string) NodeFamily {
return NodeFamily{Read: read, Prefix: prefix, c: c}
}
// connBus is pernode.Bus over this connection.
type connBus struct {
pernode.Bus
f NodeFamily
}
func (b connBus) OpenBucket(ctx context.Context, name string) (jetstream.KeyValue, error) {
module, state, _ := cutRead(b.f.Read)
return b.f.c.bound(module, state, name, func() error { return nil }).kv(ctx)
}
func cutRead(read string) (string, string, bool) { return strings.Cut(read, ".") }
func (f NodeFamily) family() pernode.Family {
return pernode.Family{Read: f.Read, Prefix: f.Prefix, Bus: connBus{Bus: pernode.OverNATS(f.c.nc), f: f}}
}
// Nodes are the nodes whose bucket of the family the bus holds now, sorted.
func (f NodeFamily) Nodes() ([]string, error) {
ctx, cancel := context.WithTimeout(context.Background(), stateTimeout)
defer cancel()
return f.family().Nodes(ctx)
}
// Node is the state of one node of the family, its bucket `<prefix><node>`, bound as Read binds a bucket.
func (f NodeFamily) Node(node string) State {
module, name, _ := cutRead(f.Read)
bucket, err := f.family().Bucket(node)
return f.c.bound(module, name, bucket, func() error { return err })
}
// Get is a key's current value, read from the bucket of the node the key names, or nil when it has none or that node
// has no bucket.
func (f NodeFamily) Get(key string) (*stdio.StateEntry, error) {
ctx, cancel := context.WithTimeout(context.Background(), stateTimeout)
defer cancel()
e, err := f.family().Get(ctx, key)
if err != nil || e == nil {
return nil, err
}
return &stdio.StateEntry{Key: e.Key(), Value: json.RawMessage(e.Value()), Revision: e.Revision()}, nil
}
// Keys is every key with a current value in every bucket of the family, sorted.
func (f NodeFamily) Keys() ([]string, error) {
ctx, cancel := context.WithTimeout(context.Background(), stateTimeout)
defer cancel()
return f.family().Keys(ctx)
}
// Watch is pernode.Family.Watch on this connection, each change handed as a State change: every bucket's current values,
// then its changes, a bucket created later included, one call at a time; the connection closing ends it.
func (f NodeFamily) Watch(key string, handle func(stdio.StateChange) error) error {
ctx, cancel := context.WithCancel(context.Background())
go func() {
for !f.c.nc.IsClosed() {
select {
case <-ctx.Done():
return
case <-f.c.nc.StatusChanged():
}
}
cancel()
}()
_, name, _ := cutRead(f.Read)
err := f.family().Watch(ctx, key, func(c pernode.Change) error {
sc := stdio.StateChange{State: name, Key: c.Entry.Key(), Op: stdio.OpPut, Revision: c.Entry.Revision(), Current: c.Current}
if c.Entry.Operation() == jetstream.KeyValuePut {
sc.Value = json.RawMessage(c.Entry.Value())
} else {
sc.Op = stdio.OpDelete
}
return handle(sc)
})
if err != nil {
cancel()
}
return err
}