Fix 10: the catalog client imports no other seat: the key codec and the source-branches follower move into mesh-delivery's sdk

Builds: design 47 G §614, §670 (ADR 0625 §4; lead answer A-144)
This commit is contained in:
2026-11-12 02:26:55 +01:00
parent fd3788c517
commit b5074699da
24 changed files with 203 additions and 153 deletions
@@ -10,22 +10,19 @@ import (
"time"
seat "git.novox.be/novox/mesh-delivery/contracts/go/seats/meshcatalog"
delivery "git.novox.be/novox/mesh-delivery/contracts/go/seats/meshdelivery"
"git.novox.be/novox/mesh-delivery/sdk/sourcebranches"
"git.novox.be/novox/mesh-sdk/go/buscalls"
"git.novox.be/novox/mesh-sdk/go/keys"
"git.novox.be/novox/mesh-sdk/go/views"
)
// SourceVerb is the gate's verb a part reads the branches the gate keeps a source revision of by, before the first
// registrations, as its manifest's invokes names it (design 47 G §614).
const SourceVerb = "seat:" + delivery.Seat + "." + delivery.VerbSource
// registrations, as its manifest's invokes names it (design 47 G §614); the follower is the SDK's sourcebranches.
const SourceVerb = sourcebranches.SourceVerb
// EntryVerb is the seat verb a part reads an entry by its fingerprint by, as its manifest's invokes names it.
const EntryVerb = "seat:" + seat.Seat + "." + seat.VerbEntry
// StateSourceBranches names the view of the branches the gate's source keeps in a part's reader (`mesh-delivery.source`).
const StateSourceBranches = "source"
// Wiring is how Branches puts a view on its part's one reader and takes it off: ReaderWiring for a bundle's
// views.Reader, ProcessWiring for a process's views.ProcessReader. Adopt is the reader's Adopt, so a view adopted after
// the reader's Start takes its first view at once; Drop is its Drop, which clears every finding the view raised.
@@ -58,96 +55,6 @@ func (w readerWiring) Rows(name string, rows func() any) { w.r.Rows(name, rows)
// ProcessWiring wires Branches on a process's reader.
func ProcessWiring(r *views.ProcessReader) Wiring { return processWiring{r} }
// SourceBranches is a part's follower of the branches the gate's `source` keeps (design 47 G §614): a first view of the
// verb `source` given no branch, then each `source-moved` heard. Its picture is keyed by the escaped branch name.
type SourceBranches struct {
Follower *views.Follower
call Call
}
// NewSourceBranches is a SourceBranches asking the gate through call. Nothing is asked before the reader starts it.
func NewSourceBranches(call Call) *SourceBranches {
s := &SourceBranches{call: call}
s.Follower = &views.Follower{Owner: delivery.Seat, Tool: SourceVerb, State: StateSourceBranches, Ask: s.ask}
return s
}
// Branches is the escaped name of each branch the picture holds.
func (s *SourceBranches) Branches() []string {
out := make([]string, 0)
for b := range s.Follower.Values() {
out = append(out, b)
}
slices.Sort(out)
return out
}
// Hear takes one `source-moved` event: a branch the picture does not hold asks the first view again until it does, on
// its own goroutine, so the consumer is never held. Data that is no such event is an error.
func (s *SourceBranches) Hear(ctx context.Context, data []byte) error {
var ev delivery.SourceMovedEvent
if err := json.Unmarshal(data, &ev); err != nil || ev.Branch == "" {
return fmt.Errorf("meshcatalogclient: not a %s event: %s", delivery.EventSourceMoved, clipBytes(data))
}
held := func() bool { _, ok := s.Follower.Get(keys.EscapeName(ev.Branch)); return ok }
if held() {
return nil
}
go catchUp(ctx, s.Follower, held, fmt.Sprintf("source of %s moved", ev.Branch))
return nil
}
func (s *SourceBranches) ask(_ context.Context, _ string) (views.Answer, error) {
raw, err := s.call(SourceVerb, delivery.SourceInput{})
if err != nil {
return views.Answer{}, err
}
answer, said, ok := buscalls.Unwrap(raw)
if !ok {
return views.Answer{}, fmt.Errorf("%s answered: %s", SourceVerb, said)
}
var a delivery.SourceAnswer
if err := json.Unmarshal(answer, &a); err != nil {
return views.Answer{}, fmt.Errorf("%s answered no branches: %s", SourceVerb, clipBytes(answer))
}
if a.Error != "" {
return views.Answer{}, fmt.Errorf("%s answered: %s", SourceVerb, a.Error)
}
values := make(map[string]json.RawMessage, len(a.Branches))
for _, b := range a.Branches {
values[b] = json.RawMessage(`{}`)
}
return views.Answer{V: 1, Owner: delivery.Seat, State: StateSourceBranches, Created: registrationsCreated, Values: values, At: time.Now()}, nil
}
// catchUp asks f's first view again until held says it holds what was heard, pausing between asks as the follower's Wait
// says (doubling to a minute), or until ctx ends: an ask already running may have been answered before the change was
// written.
func catchUp(ctx context.Context, f *views.Follower, held func() bool, why string) {
wait := f.Wait
if wait == 0 {
wait = 5 * time.Second
}
for !held() {
if err := f.Gap(ctx, why); err != nil {
return
}
if held() {
return
}
p, cancel := context.WithTimeout(ctx, wait)
<-p.Done()
cancel()
if ctx.Err() != nil {
return
}
if wait *= 2; wait > time.Minute {
wait = time.Minute
}
}
}
// Branches is the one branch follower of design 47 G §614, built once and used by every part that reads the catalog half:
// it follows the watched branches of the registrations (the first view of `registrations` at the start, then each
// `registration-changed`); before the first registration, the branches the gate's `source` answers (its first view at
@@ -168,7 +75,7 @@ type Branches struct {
w Wiring
mu sync.Mutex
source *SourceBranches
source *sourcebranches.SourceBranches
left []string // the source's branches when it was left, kept until the registrations hold one
catalogs map[string]*Catalog
order []string
@@ -180,12 +87,12 @@ func NewBranches(w Wiring, call Call) (*Branches, error) {
b := &Branches{call: call, w: w, catalogs: map[string]*Catalog{}, Entries: NewEntries(call)}
b.Registrations = NewRegistrations(call)
b.Registrations.Follower.Changed = func(string, json.RawMessage, bool, time.Time) { b.reconcile() }
b.source = NewSourceBranches(call)
b.source = sourcebranches.New(sourcebranches.Call(call))
b.source.Follower.Changed = func(string, json.RawMessage, bool, time.Time) { b.reconcile() }
if err := w.Adopt(b.Registrations.Follower, seat.Seat, seat.EventRegistrationChanged, b.hearRegistration); err != nil {
return nil, err
}
if err := w.Adopt(b.source.Follower, delivery.Seat, delivery.EventSourceMoved, b.source.Hear); err != nil {
if err := w.Adopt(b.source.Follower, sourcebranches.Seat, sourcebranches.EventSourceMoved, b.source.Hear); err != nil {
return nil, err
}
if rw, ok := w.(interface {
@@ -225,7 +132,7 @@ func (b *Branches) hearRegistration(ctx context.Context, data []byte) error {
}
b.mu.Unlock()
if s != nil {
if err := b.w.Drop(delivery.Seat, StateSourceBranches); err != nil {
if err := b.w.Drop(sourcebranches.Seat, sourcebranches.StateSourceBranches); err != nil {
b.logf("meshcatalogclient: the gate's source is not dropped: %v", err)
} else {
b.logf("meshcatalogclient: the first registration-changed is heard: the gate's source is followed no more")
@@ -11,7 +11,7 @@ import (
"time"
seat "git.novox.be/novox/mesh-delivery/contracts/go/seats/meshcatalog"
delivery "git.novox.be/novox/mesh-delivery/contracts/go/seats/meshdelivery"
"git.novox.be/novox/mesh-delivery/sdk/sourcebranches"
"git.novox.be/novox/mesh-sdk/go/views"
)
@@ -78,8 +78,8 @@ func (f *fakeSeats) call(key string, body any) (json.RawMessage, error) {
f.mu.Lock()
defer f.mu.Unlock()
switch key {
case SourceVerb:
return json.Marshal(delivery.SourceAnswer{Branches: f.source})
case sourcebranches.SourceVerb:
return json.Marshal(map[string][]string{"branches": f.source})
case RegistrationsVerb:
a := seat.RegistrationsAnswer{Registrations: []seat.Registration{}}
for m, br := range f.regs {
@@ -5,11 +5,11 @@
package meshcatalogclient
import (
"git.novox.be/novox/mesh-delivery/contracts/go/clients/meshcatalogkey"
"git.novox.be/novox/mesh-delivery/sdk/catalogkey"
)
// SourcePrefix2 opens every source fingerprint of the form.
const SourcePrefix2 = meshcatalogkey.SourcePrefix2
const SourcePrefix2 = catalogkey.SourcePrefix2
// Toolchain2 is what an artifact's toolchain enters the fingerprint as (novox/hq ADR 0405 clause 7): the artifact's
// language, the installed toolchain's version and the build module's recipe version.
@@ -3,9 +3,8 @@ module git.novox.be/novox/mesh-delivery/contracts/go/clients/meshcatalogclient
go 1.26.0
require (
git.novox.be/novox/mesh-delivery/contracts/go/clients/meshcatalogkey v0.0.0-00010101000000-000000000000
git.novox.be/novox/mesh-delivery/contracts/go/seats/meshcatalog v0.0.0-00010101000000-000000000000
git.novox.be/novox/mesh-delivery/contracts/go/seats/meshdelivery v0.0.0-00010101000000-000000000000
git.novox.be/novox/mesh-delivery/sdk v0.0.0-00010101000000-000000000000
git.novox.be/novox/mesh-sdk/go v0.0.0-00010101000000-000000000000
)
@@ -0,0 +1,22 @@
package meshcatalogclient
import (
"go/build"
"strings"
"testing"
)
// ADR 0625 §4 (design 26, a client package): a client imports its own seat's contract, mesh-sdk's runtime and its domain's
// SDK, never another seat's contract or client.
func TestTheClientImportsNoOtherSeat(t *testing.T) {
p, err := build.ImportDir(".", 0)
if err != nil {
t.Fatal(err)
}
imports := append(append([]string{}, p.Imports...), p.TestImports...)
for _, i := range imports {
if strings.Contains(i, "/seats/meshdelivery") || strings.Contains(i, "/clients/") {
t.Errorf("meshcatalogclient imports %s", i)
}
}
}
@@ -6,8 +6,8 @@ import (
"path"
"strings"
"git.novox.be/novox/mesh-delivery/contracts/go/clients/meshcatalogkey"
seat "git.novox.be/novox/mesh-delivery/contracts/go/seats/meshcatalog"
"git.novox.be/novox/mesh-delivery/sdk/catalogkey"
"git.novox.be/novox/mesh-sdk/go/keys"
)
@@ -93,7 +93,7 @@ func CheckModule(m seat.ModulesByModuleValue) error {
return fmt.Errorf("meshcatalogclient: a refused module warns: a refusal names problems")
}
if m.Fp2 != "" {
if _, err := meshcatalogkey.KeyPart(m.Fp2); err != nil {
if _, err := catalogkey.KeyPart(m.Fp2); err != nil {
return err
}
}
@@ -4,6 +4,7 @@ import (
"context"
"encoding/json"
"fmt"
"git.novox.be/novox/mesh-delivery/sdk/sourcebranches"
"slices"
"time"
@@ -18,11 +19,11 @@ const RegistrationsVerb = "seat:" + seat.Seat + "." + seat.VerbRegistrations
// Call asks one seat verb over the bus and answers what the holder said, whatever wraps it (process.Conn.Call, or
// stdio.Ask through the tool runner).
type Call func(key string, body any) (json.RawMessage, error)
type Call = sourcebranches.Call
// registrationsCreated stands for the picture's Created in its follower: the registrations are followed by the seat's
// event, never by an owner's views family, so one fixed time, never zero.
var registrationsCreated = time.Unix(0, 0).UTC()
var registrationsCreated = sourcebranches.Created
// Registrations is a part's typed follower of the registrations (design 47 G §353): a first view asked of the seat's verb
// `registrations`, then each `registration-changed` heard, built on go/views' Follower, so the runtime's tools
@@ -145,7 +146,7 @@ func (r *Registrations) holds(module string, version int64) bool {
}
func (r *Registrations) catchUp(ctx context.Context, module string, version int64) {
catchUp(ctx, r.Follower, func() bool { return r.holds(module, version) }, fmt.Sprintf("registration of %s at version %d said", module, version))
sourcebranches.CatchUp(ctx, r.Follower, func() bool { return r.holds(module, version) }, fmt.Sprintf("registration of %s at version %d said", module, version))
}
// ask is the Follower's Ask: every registration, whole in one answer.
@@ -170,9 +171,4 @@ func (r *Registrations) ask(_ context.Context, _ string) (views.Answer, error) {
return views.Answer{V: 1, Owner: seat.Seat, State: seat.StateRegistrations, Created: registrationsCreated, Values: values, At: time.Now()}, nil
}
func clipBytes(b []byte) string {
if len(b) > 200 {
return string(b[:200]) + "…"
}
return string(b)
}
func clipBytes(b []byte) string { return sourcebranches.ClipBytes(b) }
@@ -1,5 +0,0 @@
module git.novox.be/novox/mesh-delivery/contracts/go/clients/meshcatalogkey
go 1.26.0
require git.novox.be/novox/mesh-sdk/go v0.0.0-00010101000000-000000000000
+3 -3
View File
@@ -16,7 +16,7 @@ import (
"git.novox.be/novox/mesh-contracts/go/clients/meshmoduleassignerclient"
"git.novox.be/novox/mesh-contracts/go/clients/nodeengineclient"
"git.novox.be/novox/mesh-delivery/contracts/go/clients/meshcatalogkey"
"git.novox.be/novox/mesh-delivery/sdk/catalogkey"
"git.novox.be/novox/mesh-delivery/contracts/go/clients/meshdeliveryclient"
"git.novox.be/novox/mesh-delivery/modules/mesh-delivery-builder/builds"
"git.novox.be/novox/mesh-delivery/sdk/kept"
@@ -108,7 +108,7 @@ func OutsideAbout(key string) string { return strings.TrimPrefix(key, AskKindOut
// BackupAskKey is the key of the ask about a module's failed backup on a node before it applies a fingerprint:
// `backup.<node>.<module>.<hex>`.
func BackupAskKey(node, module, fingerprint string) (string, error) {
part, err := meshcatalogkey.KeyPart(fingerprint)
part, err := catalogkey.KeyPart(fingerprint)
if err != nil {
return "", err
}
@@ -185,7 +185,7 @@ func ParseAnyAskKey(key string) (AskKey, error) {
if err != nil || isNode {
return refused(fmt.Errorf("a backup ask is backup.<node>.<module>.<fingerprint hex>"))
}
fp, err := meshcatalogkey.ParseKeyPart(part)
fp, err := catalogkey.ParseKeyPart(part)
if err != nil {
return refused(err)
}
@@ -6,7 +6,6 @@ require (
git.novox.be/novox/mesh-contracts/go/clients/meshmoduleassignerclient v0.0.0-00010101000000-000000000000
git.novox.be/novox/mesh-contracts/go/clients/nodeengineclient v0.0.0-00010101000000-000000000000
git.novox.be/novox/mesh-delivery/contracts/go/clients/meshcatalogclient v0.0.0-00010101000000-000000000000
git.novox.be/novox/mesh-delivery/contracts/go/clients/meshcatalogkey v0.0.0-00010101000000-000000000000
git.novox.be/novox/mesh-delivery/contracts/go/clients/meshdeliveryclient v0.0.0-00010101000000-000000000000
git.novox.be/novox/mesh-delivery/modules/mesh-delivery-builder/builds v0.0.0-00010101000000-000000000000
git.novox.be/novox/mesh-delivery/sdk v0.0.0-00010101000000-000000000000
@@ -5,7 +5,7 @@ import (
"strings"
"git.novox.be/novox/mesh-contracts/go/clients/nodeengineclient"
"git.novox.be/novox/mesh-delivery/contracts/go/clients/meshcatalogkey"
"git.novox.be/novox/mesh-delivery/sdk/catalogkey"
)
// Every node's published compositions, read only to compare (novox/hq ADR 0593 §8): each node's engine writes its own
@@ -44,7 +44,7 @@ func published(values map[string][]byte) map[string][]publishedRow {
_, module, part, isNode, err := nodeengineclient.ParseComposedKey(key)
var fp string
if err == nil && !isNode {
fp, err = meshcatalogkey.ParseKeyPart(part)
fp, err = catalogkey.ParseKeyPart(part)
}
switch {
case err != nil:
@@ -15,7 +15,7 @@ import (
"git.novox.be/novox/mesh-contracts/go/clients/nodeengineclient"
"git.novox.be/novox/mesh-contracts/go/seats/nodeengine"
mcseat "git.novox.be/novox/mesh-contracts/health-and-repair/go/seats/meshconditions"
"git.novox.be/novox/mesh-delivery/contracts/go/clients/meshcatalogkey"
"git.novox.be/novox/mesh-delivery/sdk/catalogkey"
meshwants "git.novox.be/novox/mesh-delivery/contracts/go/seats/meshwants"
"git.novox.be/novox/mesh-delivery/modules/mesh-delivery-assigner/asks"
"git.novox.be/novox/mesh-sdk/go/seatdef"
@@ -209,7 +209,7 @@ func (p *passer) outsideReview(ctx context.Context, module, fp string, nodes []s
if o == nil || fp == "" || p.kvs[buckets.Waits] == nil {
return n
}
hex, err := meshcatalogkey.KeyPart(fp)
hex, err := catalogkey.KeyPart(fp)
if err != nil {
return n
}
@@ -17,7 +17,7 @@ import (
"git.novox.be/novox/mesh-contracts/go/clients/nodeengineclient"
"git.novox.be/novox/mesh-contracts/go/seats/nodeengine"
"git.novox.be/novox/mesh-delivery/contracts/go/clients/meshcatalogclient"
"git.novox.be/novox/mesh-delivery/contracts/go/clients/meshcatalogkey"
"git.novox.be/novox/mesh-delivery/sdk/catalogkey"
"git.novox.be/novox/mesh-delivery/contracts/go/seats/meshcatalog"
"git.novox.be/novox/mesh-sdk/go/keys"
"git.novox.be/novox/mesh-sdk/go/seatlist"
@@ -542,7 +542,7 @@ func wantsVerbs(t *testing.T) {
indexer := &fakeIndexer{views: map[string]map[string][]byte{"mesh-delivery-indexer.modules": {}, "mesh-delivery-indexer.manifests": {}}}
// ADR 0593 §6, §8: what each node published, every node's bucket as one state: anchor's own composition and its web at
// a fingerprint, and laptop's web refused there.
partNew, _ := meshcatalogkey.KeyPart(fpNew)
partNew, _ := catalogkey.KeyPart(fpNew)
webKey, _ := nodeengineclient.ComposedKey("anchor", "web", partNew)
refusedKey, _ := nodeengineclient.ComposedKey("laptop", "web", partNew)
st[buckets.ComposedRead].values["anchor.node"], _ = nodeengineclient.EncodeComposition(nodeengineclient.Composition{Node: "anchor"})
@@ -17,7 +17,7 @@ import (
"git.novox.be/novox/mesh-delivery/contracts/go/clients/meshcatalogclient"
"git.novox.be/novox/mesh-delivery/contracts/go/clients/meshcatalogkey"
"git.novox.be/novox/mesh-delivery/sdk/catalogkey"
"git.novox.be/novox/mesh-delivery/modules/mesh-delivery-builder/builds"
stdio "git.novox.be/novox/mesh-sdk/go"
@@ -423,7 +423,7 @@ func serveTools() error {
// shortFP is a fingerprint's first twelve hex digits.
func shortFP(fp string) string {
part, err := meshcatalogkey.KeyPart(fp)
part, err := catalogkey.KeyPart(fp)
if err != nil || len(part) < 12 {
return fp
}
-2
View File
@@ -51,10 +51,8 @@ require git.novox.be/novox/mesh-contracts/health-and-repair/go/seats/meshwatchdo
require git.novox.be/novox/mesh-contracts/health-and-repair/go/clients/meshwatchdogsclient v0.0.0-00010101000000-000000000000
require git.novox.be/novox/mesh-delivery/contracts/go/clients/meshcatalogclient v0.0.0-00010101000000-000000000000
require git.novox.be/novox/mesh-delivery/contracts/go/clients/meshcatalogkey v0.0.0-00010101000000-000000000000
replace git.novox.be/novox/mesh-delivery/contracts/go/clients/meshcatalogclient => ../../contracts/go/clients/meshcatalogclient
replace git.novox.be/novox/mesh-delivery/contracts/go/clients/meshcatalogkey => ../../contracts/go/clients/meshcatalogkey
require git.novox.be/novox/mesh-delivery/contracts/go/clients/meshdeliveryclient v0.0.0-00010101000000-000000000000
@@ -5,7 +5,7 @@ import (
"git.novox.be/novox/mesh-contracts/go/clients/nodeengineclient"
"git.novox.be/novox/mesh-contracts/go/seats/nodeengine"
"git.novox.be/novox/mesh-delivery/contracts/go/clients/meshcatalogclient"
"git.novox.be/novox/mesh-delivery/contracts/go/clients/meshcatalogkey"
"git.novox.be/novox/mesh-delivery/sdk/catalogkey"
mwc "git.novox.be/novox/mesh-delivery/contracts/go/clients/meshwantsclient"
"git.novox.be/novox/mesh-delivery/modules/mesh-grants/grantsstate"
"maps"
@@ -783,7 +783,7 @@ func normal(w mwc.Want) mwc.Want {
}
func short(fp string) string {
part, err := meshcatalogkey.KeyPart(fp)
part, err := catalogkey.KeyPart(fp)
if err != nil || len(part) < 12 {
return fp
}
@@ -10,7 +10,7 @@ import (
"time"
nec "git.novox.be/novox/mesh-contracts/go/clients/nodeengineclient"
"git.novox.be/novox/mesh-delivery/contracts/go/clients/meshcatalogkey"
"git.novox.be/novox/mesh-delivery/sdk/catalogkey"
"git.novox.be/novox/mesh-sdk/go/keys"
)
@@ -100,7 +100,7 @@ func (p Put) Said(at time.Time, step, words string) Put {
var digestForm = regexp.MustCompile(`^sha256:[0-9a-f]{64}$`)
func (p Put) check() error {
if _, err := meshcatalogkey.KeyPart(p.FP); err != nil {
if _, err := catalogkey.KeyPart(p.FP); err != nil {
return fmt.Errorf("puts: a put's fingerprint: %w", err)
}
if p.Why == "" {
@@ -12,7 +12,7 @@ import (
"strings"
"git.novox.be/novox/mesh-delivery/contracts/go/clients/containerimageregistryclient/reference"
"git.novox.be/novox/mesh-delivery/contracts/go/clients/meshcatalogkey"
"git.novox.be/novox/mesh-delivery/sdk/catalogkey"
"git.novox.be/novox/mesh-delivery/contracts/go/seats/containerimageregistry"
"git.novox.be/novox/mesh-delivery/contracts/go/seats/ociartifactregistry"
"git.novox.be/novox/mesh-delivery/modules/mesh-delivery-builder/builds/forms"
@@ -63,13 +63,13 @@ const (
)
// BuilderBuildKey is the key, in BuilderBuildsBucket, of a module's build at a fingerprint: `<module>.<hex>`, the hex
// being the fingerprint's key part (meshcatalogkey.KeyPart), since a bus key may not hold ':'. A module holding a dot or a
// being the fingerprint's key part (catalogkey.KeyPart), since a bus key may not hold ':'. A module holding a dot or a
// character a key refuses, and a fingerprint not of the new form, are refused, naming them.
func BuilderBuildKey(module, fp string) (string, error) {
if err := keys.CheckName("module", module); err != nil {
return "", err
}
part, err := meshcatalogkey.KeyPart(fp)
part, err := catalogkey.KeyPart(fp)
if err != nil {
return "", err
}
@@ -85,7 +85,7 @@ func ParseBuilderBuildKey(key string) (module, fp string, err error) {
if err := keys.CheckName("module", module); err != nil {
return "", "", fmt.Errorf("builds: %q is not a builder build key: %w", key, err)
}
if fp, err = meshcatalogkey.ParseKeyPart(part); err != nil {
if fp, err = catalogkey.ParseKeyPart(part); err != nil {
return "", "", fmt.Errorf("builds: %q is not a builder build key: %w", key, err)
}
return module, fp, nil
@@ -4,7 +4,6 @@ go 1.26.0
require (
git.novox.be/novox/mesh-delivery/contracts/go/clients/containerimageregistryclient v0.0.0-00010101000000-000000000000
git.novox.be/novox/mesh-delivery/contracts/go/clients/meshcatalogkey v0.0.0-00010101000000-000000000000
git.novox.be/novox/mesh-delivery/contracts/go/seats/containerimageregistry v0.0.0-00010101000000-000000000000
git.novox.be/novox/mesh-delivery/contracts/go/seats/ociartifactregistry v0.0.0-00010101000000-000000000000
git.novox.be/novox/mesh-delivery/sdk v0.0.0-00010101000000-000000000000
-1
View File
@@ -8,7 +8,6 @@ require (
git.novox.be/novox/mesh-contracts/go/seats/nodeengine v0.0.0-00010101000000-000000000000
git.novox.be/novox/mesh-contracts/health-and-repair/go/clients/meshhandactsclient v0.0.0-00010101000000-000000000000
git.novox.be/novox/mesh-delivery/contracts/go/clients/meshcatalogclient v0.0.0-00010101000000-000000000000
git.novox.be/novox/mesh-delivery/contracts/go/clients/meshcatalogkey v0.0.0-00010101000000-000000000000
git.novox.be/novox/mesh-delivery/contracts/go/clients/meshdeliverycheckerclient v0.0.0-00010101000000-000000000000
git.novox.be/novox/mesh-delivery/contracts/go/clients/meshdeliveryclient v0.0.0-00010101000000-000000000000
git.novox.be/novox/mesh-delivery/contracts/go/seats/git v0.0.0-00010101000000-000000000000
@@ -9,7 +9,7 @@ import (
"git.novox.be/novox/mesh-delivery/contracts/go/clients/meshcatalogclient"
"git.novox.be/novox/mesh-delivery/contracts/go/clients/meshcatalogkey"
"git.novox.be/novox/mesh-delivery/sdk/catalogkey"
"git.novox.be/novox/mesh-delivery/contracts/go/clients/meshdeliveryclient"
"git.novox.be/novox/mesh-delivery/contracts/go/seats/meshcatalog"
"git.novox.be/novox/mesh-sdk/go/keys"
@@ -52,7 +52,7 @@ func checkCandidateModule(m meshcatalog.CandidatesByGroupValuePullsItemFpsValue)
return err
}
if m.Fp2 != "" {
if _, err := meshcatalogkey.KeyPart(m.Fp2); err != nil {
if _, err := catalogkey.KeyPart(m.Fp2); err != nil {
return err
}
}
@@ -1,7 +1,7 @@
// Package meshcatalogkey is the codec of a catalog key's parts (design 47 G §670): a fingerprint as the part of a bus
// Package catalogkey is the codec of a catalog key's parts (design 47 G §670): a fingerprint as the part of a bus
// key, and back. It imports neither mesh-sdk's views nor the bus client, so a package that only reads and writes keys
// (the builder's builds package) reaches no bus; meshcatalogclient imports it.
package meshcatalogkey
package catalogkey
import (
"fmt"
@@ -1,4 +1,4 @@
package meshcatalogkey
package catalogkey
import (
"go/build"
@@ -33,7 +33,7 @@ func TestThePackageImportsNoBus(t *testing.T) {
}
for _, i := range p.Imports {
if strings.Contains(i, "nats") || strings.Contains(i, "/views") {
t.Errorf("meshcatalogkey imports %s", i)
t.Errorf("catalogkey imports %s", i)
}
}
}
+136
View File
@@ -0,0 +1,136 @@
// Package sourcebranches is the follower of the branches the seat mesh-delivery's verb source keeps (design 47 G §614),
// in the Delivery domain's SDK so that a seat's client holds no other seat's types (ADR 0625 §4): the first view of the
// verb given no branch, then each source-moved heard, and the asking again until a change heard is held.
package sourcebranches
import (
"context"
"encoding/json"
"fmt"
"slices"
"time"
delivery "git.novox.be/novox/mesh-delivery/contracts/go/seats/meshdelivery"
"git.novox.be/novox/mesh-sdk/go/buscalls"
"git.novox.be/novox/mesh-sdk/go/keys"
"git.novox.be/novox/mesh-sdk/go/views"
)
// Seat is the seat whose verb source and event source-moved are followed.
const Seat = delivery.Seat
// EventSourceMoved is the event the seat says when a branch's source moved.
const EventSourceMoved = delivery.EventSourceMoved
// SourceVerb is the gate's verb a part reads the branches the gate keeps a source revision of by, before the first
// registrations, as its manifest's invokes names it (design 47 G §614).
const SourceVerb = "seat:" + delivery.Seat + "." + delivery.VerbSource
// StateSourceBranches names the view of the branches the gate's source keeps in a part's reader (`mesh-delivery.source`).
const StateSourceBranches = "source"
// Call asks one seat verb over the bus and answers what the holder said, whatever wraps it (process.Conn.Call, or
// stdio.Ask through the tool runner).
type Call func(key string, body any) (json.RawMessage, error)
// Created stands for the picture's Created in a follower: followed by the seat's event, never by an owner's views family,
// so one fixed time, never zero.
var Created = time.Unix(0, 0).UTC()
// ClipBytes is data cut to 200 bytes for a message.
func ClipBytes(b []byte) string {
if len(b) > 200 {
return string(b[:200]) + "…"
}
return string(b)
}
// SourceBranches is a part's follower of the branches the gate's `source` keeps (design 47 G §614): a first view of the
// verb `source` given no branch, then each `source-moved` heard. Its picture is keyed by the escaped branch name.
type SourceBranches struct {
Follower *views.Follower
call Call
}
// New is a SourceBranches asking the gate through call. Nothing is asked before the reader starts it.
func New(call Call) *SourceBranches {
s := &SourceBranches{call: call}
s.Follower = &views.Follower{Owner: Seat, Tool: SourceVerb, State: StateSourceBranches, Ask: s.ask}
return s
}
// Branches is the escaped name of each branch the picture holds.
func (s *SourceBranches) Branches() []string {
out := make([]string, 0)
for b := range s.Follower.Values() {
out = append(out, b)
}
slices.Sort(out)
return out
}
// Hear takes one `source-moved` event: a branch the picture does not hold asks the first view again until it does, on
// its own goroutine, so the consumer is never held. Data that is no such event is an error.
func (s *SourceBranches) Hear(ctx context.Context, data []byte) error {
var ev delivery.SourceMovedEvent
if err := json.Unmarshal(data, &ev); err != nil || ev.Branch == "" {
return fmt.Errorf("sourcebranches: not a %s event: %s", EventSourceMoved, ClipBytes(data))
}
held := func() bool { _, ok := s.Follower.Get(keys.EscapeName(ev.Branch)); return ok }
if held() {
return nil
}
go CatchUp(ctx, s.Follower, held, fmt.Sprintf("source of %s moved", ev.Branch))
return nil
}
func (s *SourceBranches) ask(_ context.Context, _ string) (views.Answer, error) {
raw, err := s.call(SourceVerb, delivery.SourceInput{})
if err != nil {
return views.Answer{}, err
}
answer, said, ok := buscalls.Unwrap(raw)
if !ok {
return views.Answer{}, fmt.Errorf("%s answered: %s", SourceVerb, said)
}
var a delivery.SourceAnswer
if err := json.Unmarshal(answer, &a); err != nil {
return views.Answer{}, fmt.Errorf("%s answered no branches: %s", SourceVerb, ClipBytes(answer))
}
if a.Error != "" {
return views.Answer{}, fmt.Errorf("%s answered: %s", SourceVerb, a.Error)
}
values := make(map[string]json.RawMessage, len(a.Branches))
for _, b := range a.Branches {
values[b] = json.RawMessage(`{}`)
}
return views.Answer{V: 1, Owner: Seat, State: StateSourceBranches, Created: Created, Values: values, At: time.Now()}, nil
}
// CatchUp asks f's first view again until held says it holds what was heard, pausing between asks as the follower's Wait
// says (doubling to a minute), or until ctx ends: an ask already running may have been answered before the change was
// written.
func CatchUp(ctx context.Context, f *views.Follower, held func() bool, why string) {
wait := f.Wait
if wait == 0 {
wait = 5 * time.Second
}
for !held() {
if err := f.Gap(ctx, why); err != nil {
return
}
if held() {
return
}
p, cancel := context.WithTimeout(ctx, wait)
<-p.Done()
cancel()
if ctx.Err() != nil {
return
}
if wait *= 2; wait > time.Minute {
wait = time.Minute
}
}
}