The seat list's five-minute finding and the finding of a held module are raised as they happen, so they use the held writer (seeded at Start, Held() answers what stands) over the contract of the seat mesh-conditions; the keepers keep no set or sweep of their own, and the writer is started before the seat list reader writes or clears any. The engine's own finding stream stays its foundation stream mesh-host-findings. Builds: design 47 G §529, A-29, A-30, A-37, A-38, A-40 Claude-Session: https://claude.ai/code/session_01NcbgfGKaxw984XyAPfLm4h
192 lines
5.9 KiB
Go
192 lines
5.9 KiB
Go
package link
|
|
|
|
import (
|
|
"context"
|
|
"encoding/json"
|
|
"errors"
|
|
"fmt"
|
|
|
|
stdio "git.novox.be/novox/mesh-sdk/go"
|
|
"git.novox.be/novox/mesh-sdk/go/views"
|
|
"github.com/nats-io/nats.go"
|
|
)
|
|
|
|
// FindingsBucket is the node-engine's state `findings`: the findings this engine writes in its own bucket, one key each,
|
|
// which the condition keeper raises as conditions and clears when the key goes (novox/hq ADR 0564 §2, ADR 0636 §1).
|
|
const FindingsBucket = "mesh-host_findings"
|
|
|
|
func (l *natsLink) findingsKV() (nats.KeyValue, error) {
|
|
kv, err := l.js.KeyValue(FindingsBucket)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("the node-engine's state %s cannot be bound (it is created from the node-engine's manifest; "+
|
|
"written again once it is): %w", FindingsBucket, err)
|
|
}
|
|
return kv, nil
|
|
}
|
|
|
|
func (q *Session) findings() (nats.KeyValue, error) {
|
|
q.init()
|
|
q.mu.Lock()
|
|
bus := q.bus
|
|
q.mu.Unlock()
|
|
l, ok := bus.(interface{ findingsKV() (nats.KeyValue, error) })
|
|
if bus == nil || !ok {
|
|
return nil, ErrNoLink
|
|
}
|
|
return l.findingsKV()
|
|
}
|
|
|
|
// PutFinding writes one finding of this engine over the link open now.
|
|
func (q *Session) PutFinding(key string, value []byte) error {
|
|
kv, err := q.findings()
|
|
if err != nil {
|
|
return err
|
|
}
|
|
_, err = kv.Put(key, value)
|
|
return err
|
|
}
|
|
|
|
// FindingKeys lists the keys of this engine's findings over the link open now; none when the bucket holds none.
|
|
func (q *Session) FindingKeys() ([]string, error) {
|
|
kv, err := q.findings()
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
keys, err := kv.Keys()
|
|
if err != nil && !errors.Is(err, nats.ErrNoKeysFound) {
|
|
return nil, err
|
|
}
|
|
return keys, nil
|
|
}
|
|
|
|
// DeleteFinding clears one finding of this engine over the link open now; a finding already gone is cleared.
|
|
func (q *Session) DeleteFinding(key string) error {
|
|
kv, err := q.findings()
|
|
if err != nil {
|
|
return err
|
|
}
|
|
if err = kv.Delete(key); err != nil && !errors.Is(err, nats.ErrKeyNotFound) {
|
|
return err
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func (o OverNATS) findingsKV() (nats.KeyValue, error) {
|
|
kv, err := o.JS.KeyValue(FindingsBucket)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("the node-engine's state %s cannot be bound: %w", FindingsBucket, err)
|
|
}
|
|
return kv, nil
|
|
}
|
|
|
|
// The node-engine's finding writer (mesh-sdk views.Writer, design 47 G §383, §384, §461) is made over the link open now:
|
|
// its state is the bucket above, its events are published on the engine's own stream awaiting the bus's acknowledgement, and
|
|
// its consumer of `findings-asked` is its own, from the next ask on.
|
|
|
|
// FindingsState is the node-engine's state `findings` over the link open now, as the finding writer reads it
|
|
// (views.FindingState).
|
|
type FindingsState struct{ Session *Session }
|
|
|
|
// Get is the key's entry, or none for a key the state does not hold.
|
|
func (s FindingsState) Get(key string) (*stdio.StateEntry, error) {
|
|
kv, err := s.Session.findings()
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
e, err := kv.Get(key)
|
|
if errors.Is(err, nats.ErrKeyNotFound) {
|
|
return nil, nil
|
|
}
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
return &stdio.StateEntry{Key: key, Value: e.Value(), Revision: e.Revision()}, nil
|
|
}
|
|
|
|
// Put writes the value as JSON under the key.
|
|
func (s FindingsState) Put(key string, value any) (uint64, error) {
|
|
raw, err := json.Marshal(value)
|
|
if err != nil {
|
|
return 0, err
|
|
}
|
|
kv, err := s.Session.findings()
|
|
if err != nil {
|
|
return 0, err
|
|
}
|
|
return kv.Put(key, raw)
|
|
}
|
|
|
|
// Delete deletes the key; a key already gone is deleted.
|
|
func (s FindingsState) Delete(key string) error { return s.Session.DeleteFinding(key) }
|
|
|
|
// Keys lists the keys the state holds.
|
|
func (s FindingsState) Keys() ([]string, error) { return s.Session.FindingKeys() }
|
|
|
|
var _ views.FindingState = FindingsState{}
|
|
|
|
// eventsOver is a link that publishes on a stream and makes a consumer of its own on another.
|
|
type eventsOver interface {
|
|
publishEvent(ctx context.Context, subject string, data []byte, id string) error
|
|
listenFrom(stream, subject string, handle func([]byte)) (func(), error)
|
|
}
|
|
|
|
func publishEvent(js nats.JetStreamContext, ctx context.Context, subject string, data []byte, id string) error {
|
|
_, err := js.Publish(subject, data, nats.MsgId(id), nats.Context(ctx))
|
|
return err
|
|
}
|
|
|
|
func listenFrom(js nats.JetStreamContext, stream, subject string, handle func([]byte)) (func(), error) {
|
|
sub, err := js.Subscribe(subject, func(m *nats.Msg) { handle(m.Data) }, nats.BindStream(stream), nats.DeliverNew(), nats.AckNone())
|
|
if err != nil {
|
|
return nil, fmt.Errorf("a consumer of %s: %w", subject, err)
|
|
}
|
|
return func() { _ = sub.Unsubscribe() }, nil
|
|
}
|
|
|
|
func (l *natsLink) publishEvent(ctx context.Context, subject string, data []byte, id string) error {
|
|
return publishEvent(l.js, ctx, subject, data, id)
|
|
}
|
|
|
|
func (l *natsLink) listenFrom(stream, subject string, handle func([]byte)) (func(), error) {
|
|
return listenFrom(l.js, stream, subject, handle)
|
|
}
|
|
|
|
func (o OverNATS) publishEvent(ctx context.Context, subject string, data []byte, id string) error {
|
|
return publishEvent(o.JS, ctx, subject, data, id)
|
|
}
|
|
|
|
func (o OverNATS) listenFrom(stream, subject string, handle func([]byte)) (func(), error) {
|
|
return listenFrom(o.JS, stream, subject, handle)
|
|
}
|
|
|
|
func (q *Session) events() (eventsOver, error) {
|
|
q.init()
|
|
q.mu.Lock()
|
|
bus := q.bus
|
|
q.mu.Unlock()
|
|
e, ok := bus.(eventsOver)
|
|
if bus == nil || !ok {
|
|
return nil, ErrNoLink
|
|
}
|
|
return e, nil
|
|
}
|
|
|
|
// PublishEvent publishes one message awaiting the bus's acknowledgement, over the link open now (views.WriterConfig.Publish).
|
|
func (q *Session) PublishEvent(ctx context.Context, subject string, data []byte, id string) error {
|
|
e, err := q.events()
|
|
if err != nil {
|
|
return err
|
|
}
|
|
return e.publishEvent(ctx, subject, data, id)
|
|
}
|
|
|
|
// ListenFrom makes a consumer of its own on stream for subject that delivers only the messages published after it is
|
|
// made, over the link open now, and answers how to end it (views.WriterConfig.Listen).
|
|
func (q *Session) ListenFrom(stream, subject string, handle func([]byte)) (func(), error) {
|
|
e, err := q.events()
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
return e.listenFrom(stream, subject, handle)
|
|
}
|