Files
mesh-sdk/go/parts/bus.go
T
jschoubben 232f38de99 R2-G-catalog-B 5: the idle watch looks at every open transfer once it is taken
A transfer whose idle key ended while the holder's bundle was down had its marker seen by no watch, so it
stayed open for good. FollowIdle now hands every open transfer (IDs) to Expired once the watch is taken:
one past its bound is dropped, one within it armed again for the rest.

Claude-Session: https://claude.ai/code/session_01JG73KJNrH5EgFXMKKYSrig
2026-11-04 18:09:37 +01:00

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
}
}