Files
mesh-delivery/sdk/parts/bus.go
T

155 lines
5.3 KiB
Go

package parts
import (
"encoding/json"
"errors"
"fmt"
"sync/atomic"
"time"
stdio "git.novox.be/novox/mesh-sdk/go"
"git.novox.be/novox/mesh-sdk/go/busstate"
)
// BusObjects is a Receiver's Objects over the holder's own object store, asked for in `requires` by local (the
// template objects, novox/hq ADR 0595 §2), through the runtime: every holder binds the same adapter, none its own.
func BusObjects(local string) Objects { return busObjects{busstate.ObjectsOf(local)} }
type busObjects struct{ o busstate.Objects }
func (b busObjects) Put(name string, data []byte) error {
_, err := b.o.Put(name, data, "")
return err
}
func (b busObjects) Get(name string) ([]byte, error) {
d, err := b.o.Get(name)
return d.Data, err
}
func (b busObjects) Delete(name string) error { return b.o.Delete(name) }
func (b busObjects) Used() (uint64, error) {
f, err := b.o.Fill()
return f.Bytes, err
}
// BusTransfers is a Receiver's Transfers over the holder's own key-value state of that name (the template state),
// through the runtime, each transfer under its id.
func BusTransfers[M any](state string) Transfers[M] { return busTransfers[M]{state} }
type busTransfers[M any] struct{ state string }
func (b busTransfers[M]) Get(id string) (Transfer[M], bool, error) {
var t Transfer[M]
e, err := stdio.State(b.state).Get(id)
if err != nil || e == nil {
return t, false, err
}
return t, true, json.Unmarshal(e.Value, &t)
}
func (b busTransfers[M]) Put(t Transfer[M]) error {
if t.ID == "" {
return errors.New("a transfer without an id cannot be kept")
}
_, err := stdio.State(b.state).Put(t.ID, t)
return err
}
func (b busTransfers[M]) Delete(id string) error { return stdio.State(b.state).Delete(id) }
func (b busTransfers[M]) List() ([]Transfer[M], error) {
keys, err := stdio.State(b.state).Keys()
if err != nil {
return nil, err
}
var out []Transfer[M]
for _, k := range keys {
if t, ok, err := b.Get(k); err == nil && ok {
out = append(out, t)
}
}
return out, nil
}
// BusCap is a Receiver's Bound over the holder's own object store of that name: the cap its binding carries (the bus's
// own bound, the store's max bytes), read through the runtime once it answers one, since a binding's cap does not
// change while the bundle runs. Make it once per bundle, not per receiver.
func BusCap(local string) func() (uint64, error) {
var known atomic.Uint64
return func() (uint64, error) {
if c := known.Load(); c > 0 {
return c, nil
}
f, err := busstate.ObjectsOf(local).Fill()
if err != nil {
return 0, fmt.Errorf("%s's cap cannot be read from its binding: %w", local, err)
}
if f.MaxBytes == 0 {
return 0, fmt.Errorf("%s's binding carries no cap", local)
}
known.Store(f.MaxBytes)
return f.MaxBytes, nil
}
}
// BusArm is a Receiver's Arm over the holder's own key-value state of that name, declared key-ttl: one key per open
// transfer with its own time to live; a key already there is the same bound, kept.
func BusArm(state string) func(id string, ttl time.Duration) error {
return func(id string, ttl time.Duration) error {
if _, err := stdio.State(state).CreateTTL(id, map[string]any{"transfer": id}, ttl); err != nil && !errors.Is(err, stdio.ErrChanged) {
return err
}
return nil
}
}
// FollowIdle follows the holder's idle state (the one BusArm writes) once the bundle serves, and hands the end of each
// key (the bus's marker, OpExpire) to expired, a Receiver's Expired: the event drops an idle transfer, no timer runs.
// Once the watch is taken it hands every transfer open (open, such as IDs of the holder's Transfers) to expired too, so
// a key that ended while the bundle was down, whose marker no watch saw, still drops its transfer, and a key lost is
// armed again. said is told each outcome (dropped, or the error that stopped it) for the holder's log. It returns
// once the start's look is done; run it in its own goroutine.
func FollowIdle(state string, open func() ([]string, error), expired func(id string) (bool, error), said func(id string, dropped bool, err error)) {
<-stdio.Served()
stdio.State(state).Follow("", func(ch stdio.StateChange) error {
if ch.Op != stdio.OpExpire {
return nil
}
dropped, err := expired(ch.Key)
if said != nil && (dropped || err != nil) {
said(ch.Key, dropped, err)
}
return err
})
expireOpen(state, open, expired, said)
}
// expireOpen is FollowIdle's look at its start: every open transfer handed to expired, as its key's end would.
func expireOpen(state string, open func() ([]string, error), expired func(id string) (bool, error), said func(id string, dropped bool, err error)) {
say := func(id string, dropped bool, err error) {
if said != nil && (dropped || err != nil) {
said(id, dropped, err)
}
}
ids, err := open()
if err != nil {
say("", false, fmt.Errorf("the open transfers cannot be read at the start of %s's watch, so one whose idle key ended while the bundle was down waits for a look by hand: %w", state, err))
return
}
for _, id := range ids {
dropped, err := expired(id)
say(id, dropped, err)
}
}
// IDs answers the ids of the transfers open in t: FollowIdle's open.
func IDs[M any](t Transfers[M]) func() ([]string, error) {
return func() ([]string, error) {
all, err := t.List()
if err != nil {
return nil, err
}
out := make([]string, 0, len(all))
for _, x := range all {
out = append(out, x.ID)
}
return out, nil
}
}