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-`), 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 ``, 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 }