From b5074699da98e7c88a7077120a80430ff45ec746 Mon Sep 17 00:00:00 2001 From: jochen Date: Thu, 12 Nov 2026 02:26:55 +0100 Subject: [PATCH] Fix 10: the catalog client imports no other seat: the key codec and the source-branches follower move into mesh-delivery's sdk MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Builds: design 47 G §614, §670 (ADR 0625 §4; lead answer A-144) --- .../go/clients/meshcatalogclient/branches.go | 107 +------------- .../meshcatalogclient/branches_test.go | 6 +- .../clients/meshcatalogclient/fingerprint.go | 4 +- contracts/go/clients/meshcatalogclient/go.mod | 3 +- .../clients/meshcatalogclient/imports_test.go | 22 +++ .../go/clients/meshcatalogclient/module.go | 4 +- .../meshcatalogclient/registrations.go | 14 +- contracts/go/clients/meshcatalogkey/go.mod | 5 - modules/mesh-delivery-assigner/asks/asks.go | 6 +- modules/mesh-delivery-assigner/asks/go.mod | 1 - .../cmd/mesh-delivery-assigner/composed.go | 4 +- .../cmd/mesh-delivery-assigner/outside.go | 4 +- .../mesh-delivery-assigner/seat_tools_test.go | 4 +- .../cmd/mesh-delivery-assigner/tools.go | 4 +- modules/mesh-delivery-assigner/go.mod | 2 - .../internal/assign/wants.go | 4 +- .../internal/puts/puts.go | 4 +- .../mesh-delivery-builder/builds/builds.go | 8 +- modules/mesh-delivery-builder/builds/go.mod | 1 - modules/mesh-delivery-indexer/go.mod | 1 - .../internal/index/candidate_values.go | 4 +- .../meshcatalogkey => sdk/catalogkey}/key.go | 4 +- .../catalogkey}/key_test.go | 4 +- sdk/sourcebranches/sourcebranches.go | 136 ++++++++++++++++++ 24 files changed, 203 insertions(+), 153 deletions(-) create mode 100644 contracts/go/clients/meshcatalogclient/imports_test.go delete mode 100644 contracts/go/clients/meshcatalogkey/go.mod rename {contracts/go/clients/meshcatalogkey => sdk/catalogkey}/key.go (89%) rename {contracts/go/clients/meshcatalogkey => sdk/catalogkey}/key_test.go (93%) create mode 100644 sdk/sourcebranches/sourcebranches.go diff --git a/contracts/go/clients/meshcatalogclient/branches.go b/contracts/go/clients/meshcatalogclient/branches.go index dd606307..7c10d232 100644 --- a/contracts/go/clients/meshcatalogclient/branches.go +++ b/contracts/go/clients/meshcatalogclient/branches.go @@ -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") diff --git a/contracts/go/clients/meshcatalogclient/branches_test.go b/contracts/go/clients/meshcatalogclient/branches_test.go index b69ef1d4..c2324293 100644 --- a/contracts/go/clients/meshcatalogclient/branches_test.go +++ b/contracts/go/clients/meshcatalogclient/branches_test.go @@ -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 { diff --git a/contracts/go/clients/meshcatalogclient/fingerprint.go b/contracts/go/clients/meshcatalogclient/fingerprint.go index 3bf2a7b9..af64bd7b 100644 --- a/contracts/go/clients/meshcatalogclient/fingerprint.go +++ b/contracts/go/clients/meshcatalogclient/fingerprint.go @@ -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. diff --git a/contracts/go/clients/meshcatalogclient/go.mod b/contracts/go/clients/meshcatalogclient/go.mod index 285aebe0..d53c1dfc 100644 --- a/contracts/go/clients/meshcatalogclient/go.mod +++ b/contracts/go/clients/meshcatalogclient/go.mod @@ -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 ) diff --git a/contracts/go/clients/meshcatalogclient/imports_test.go b/contracts/go/clients/meshcatalogclient/imports_test.go new file mode 100644 index 00000000..e49071a1 --- /dev/null +++ b/contracts/go/clients/meshcatalogclient/imports_test.go @@ -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) + } + } +} diff --git a/contracts/go/clients/meshcatalogclient/module.go b/contracts/go/clients/meshcatalogclient/module.go index be56b854..277e482f 100644 --- a/contracts/go/clients/meshcatalogclient/module.go +++ b/contracts/go/clients/meshcatalogclient/module.go @@ -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 } } diff --git a/contracts/go/clients/meshcatalogclient/registrations.go b/contracts/go/clients/meshcatalogclient/registrations.go index 5bb9acc0..47a262c2 100644 --- a/contracts/go/clients/meshcatalogclient/registrations.go +++ b/contracts/go/clients/meshcatalogclient/registrations.go @@ -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) } diff --git a/contracts/go/clients/meshcatalogkey/go.mod b/contracts/go/clients/meshcatalogkey/go.mod deleted file mode 100644 index e6b41bd8..00000000 --- a/contracts/go/clients/meshcatalogkey/go.mod +++ /dev/null @@ -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 diff --git a/modules/mesh-delivery-assigner/asks/asks.go b/modules/mesh-delivery-assigner/asks/asks.go index 0ddf4dba..01cc61cd 100644 --- a/modules/mesh-delivery-assigner/asks/asks.go +++ b/modules/mesh-delivery-assigner/asks/asks.go @@ -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...`. 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...")) } - fp, err := meshcatalogkey.ParseKeyPart(part) + fp, err := catalogkey.ParseKeyPart(part) if err != nil { return refused(err) } diff --git a/modules/mesh-delivery-assigner/asks/go.mod b/modules/mesh-delivery-assigner/asks/go.mod index f3d1affe..6ed0f8c5 100644 --- a/modules/mesh-delivery-assigner/asks/go.mod +++ b/modules/mesh-delivery-assigner/asks/go.mod @@ -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 diff --git a/modules/mesh-delivery-assigner/cmd/mesh-delivery-assigner/composed.go b/modules/mesh-delivery-assigner/cmd/mesh-delivery-assigner/composed.go index 2ce918da..82b95a25 100644 --- a/modules/mesh-delivery-assigner/cmd/mesh-delivery-assigner/composed.go +++ b/modules/mesh-delivery-assigner/cmd/mesh-delivery-assigner/composed.go @@ -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: diff --git a/modules/mesh-delivery-assigner/cmd/mesh-delivery-assigner/outside.go b/modules/mesh-delivery-assigner/cmd/mesh-delivery-assigner/outside.go index 12844b81..6857216e 100644 --- a/modules/mesh-delivery-assigner/cmd/mesh-delivery-assigner/outside.go +++ b/modules/mesh-delivery-assigner/cmd/mesh-delivery-assigner/outside.go @@ -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 } diff --git a/modules/mesh-delivery-assigner/cmd/mesh-delivery-assigner/seat_tools_test.go b/modules/mesh-delivery-assigner/cmd/mesh-delivery-assigner/seat_tools_test.go index 67087e98..8f486829 100644 --- a/modules/mesh-delivery-assigner/cmd/mesh-delivery-assigner/seat_tools_test.go +++ b/modules/mesh-delivery-assigner/cmd/mesh-delivery-assigner/seat_tools_test.go @@ -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"}) diff --git a/modules/mesh-delivery-assigner/cmd/mesh-delivery-assigner/tools.go b/modules/mesh-delivery-assigner/cmd/mesh-delivery-assigner/tools.go index e57aad93..d7921b8c 100644 --- a/modules/mesh-delivery-assigner/cmd/mesh-delivery-assigner/tools.go +++ b/modules/mesh-delivery-assigner/cmd/mesh-delivery-assigner/tools.go @@ -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 } diff --git a/modules/mesh-delivery-assigner/go.mod b/modules/mesh-delivery-assigner/go.mod index f9a537fe..e7424a86 100644 --- a/modules/mesh-delivery-assigner/go.mod +++ b/modules/mesh-delivery-assigner/go.mod @@ -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 diff --git a/modules/mesh-delivery-assigner/internal/assign/wants.go b/modules/mesh-delivery-assigner/internal/assign/wants.go index 1d278f17..d0418df9 100644 --- a/modules/mesh-delivery-assigner/internal/assign/wants.go +++ b/modules/mesh-delivery-assigner/internal/assign/wants.go @@ -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 } diff --git a/modules/mesh-delivery-assigner/internal/puts/puts.go b/modules/mesh-delivery-assigner/internal/puts/puts.go index 58a8e5c0..58a6d053 100644 --- a/modules/mesh-delivery-assigner/internal/puts/puts.go +++ b/modules/mesh-delivery-assigner/internal/puts/puts.go @@ -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 == "" { diff --git a/modules/mesh-delivery-builder/builds/builds.go b/modules/mesh-delivery-builder/builds/builds.go index 0f1b118e..59a765e5 100644 --- a/modules/mesh-delivery-builder/builds/builds.go +++ b/modules/mesh-delivery-builder/builds/builds.go @@ -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: `.`, 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 diff --git a/modules/mesh-delivery-builder/builds/go.mod b/modules/mesh-delivery-builder/builds/go.mod index 4fb85e3c..2ce35b5e 100644 --- a/modules/mesh-delivery-builder/builds/go.mod +++ b/modules/mesh-delivery-builder/builds/go.mod @@ -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 diff --git a/modules/mesh-delivery-indexer/go.mod b/modules/mesh-delivery-indexer/go.mod index 6f730fc6..0770c340 100644 --- a/modules/mesh-delivery-indexer/go.mod +++ b/modules/mesh-delivery-indexer/go.mod @@ -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 diff --git a/modules/mesh-delivery-indexer/internal/index/candidate_values.go b/modules/mesh-delivery-indexer/internal/index/candidate_values.go index 17b8f5a7..e4ec0401 100644 --- a/modules/mesh-delivery-indexer/internal/index/candidate_values.go +++ b/modules/mesh-delivery-indexer/internal/index/candidate_values.go @@ -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 } } diff --git a/contracts/go/clients/meshcatalogkey/key.go b/sdk/catalogkey/key.go similarity index 89% rename from contracts/go/clients/meshcatalogkey/key.go rename to sdk/catalogkey/key.go index 17d65d55..57dcb168 100644 --- a/contracts/go/clients/meshcatalogkey/key.go +++ b/sdk/catalogkey/key.go @@ -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" diff --git a/contracts/go/clients/meshcatalogkey/key_test.go b/sdk/catalogkey/key_test.go similarity index 93% rename from contracts/go/clients/meshcatalogkey/key_test.go rename to sdk/catalogkey/key_test.go index f8051de5..eca74c89 100644 --- a/contracts/go/clients/meshcatalogkey/key_test.go +++ b/sdk/catalogkey/key_test.go @@ -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) } } } diff --git a/sdk/sourcebranches/sourcebranches.go b/sdk/sourcebranches/sourcebranches.go new file mode 100644 index 00000000..d544f228 --- /dev/null +++ b/sdk/sourcebranches/sourcebranches.go @@ -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 + } + } +}