From 8c5f85e86bfac99a7c549c3e8c2a15b3e66b1fac Mon Sep 17 00:00:00 2001 From: jochen Date: Thu, 5 Nov 2026 15:16:34 +0100 Subject: [PATCH] =?UTF-8?q?node-engine=20client:=20the=20node=20key=20of?= =?UTF-8?q?=20the=20have,=20the=20codecs=20of=20the=20health=20report=20an?= =?UTF-8?q?d=20of=20the=20data=20items=20(ADR=200634=20=C2=A71,=20=C2=A74;?= =?UTF-8?q?=20issue=201966)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Claude-Session: https://claude.ai/code/session_01NcbgfFSnn24M3GcqttoU7i --- go/clients/nodeengineclient/have_test.go | 43 +++++++++++++ go/clients/nodeengineclient/health.go | 80 ++++++++++++++++++++++++ go/clients/nodeengineclient/keys.go | 8 +++ 3 files changed, 131 insertions(+) create mode 100644 go/clients/nodeengineclient/health.go diff --git a/go/clients/nodeengineclient/have_test.go b/go/clients/nodeengineclient/have_test.go index e91d135..4b93f33 100644 --- a/go/clients/nodeengineclient/have_test.go +++ b/go/clients/nodeengineclient/have_test.go @@ -105,3 +105,46 @@ func TestHaveNodeCodec(t *testing.T) { t.Error("a record differs by its version") } } + +func TestHealthAndDataCodecs(t *testing.T) { + at := time.Date(2026, 11, 5, 12, 0, 0, 0, time.UTC) + h := seat.HealthByNodeValue{Node: "n", At: at, Fallback: &seat.LoginFallback{Node: "n", From: "old", Why: "w", At: at}} + raw, err := EncodeHealth(h) + if err != nil { + t.Fatal(err) + } + back, err := DecodeHealth(raw) + h.V = keys.Version + if err != nil || !SameHealth(back, h) { + t.Fatalf("round trip: %v %+v", err, back) + } + later := h + later.At = at.Add(time.Hour) + if !SameHealth(h, later) { + t.Error("a report differs by its time of writing") + } + for name, bad := range map[string]seat.HealthByNodeValue{ + "no node": {}, + "another node": {Node: "n", Fallback: &seat.LoginFallback{Node: "m", From: "o", Why: "w", At: at}}, + "no time": {Node: "n", Fallback: &seat.LoginFallback{Node: "n", From: "o", Why: "w"}}, + "no login from": {Node: "n", Fallback: &seat.LoginFallback{Node: "n", Why: "w", At: at}}, + } { + if _, err := EncodeHealth(bad); err == nil { + t.Errorf("%s: encoded", name) + } + } + if _, err := DecodeHealth([]byte(`{"v":2,"node":"n"}`)); !errors.Is(err, keys.ErrUnknownVersion) { + t.Errorf("version 2: %v", err) + } + d := seat.DataByNodeModuleValue{Node: "n", Module: "m", Digest: digestA} + raw, err = EncodeData(d) + if err != nil || !strings.Contains(string(raw), `"items":[]`) { + t.Fatalf("data: %v %s", err, raw) + } + if _, err := EncodeData(seat.DataByNodeModuleValue{Node: "n", Module: "node"}); err == nil { + t.Error("the module name node keys") + } + if k, err := HaveNodeKey("n"); err != nil || k != "n.node" { + t.Errorf("node key %q %v", k, err) + } +} diff --git a/go/clients/nodeengineclient/health.go b/go/clients/nodeengineclient/health.go new file mode 100644 index 0000000..b893d43 --- /dev/null +++ b/go/clients/nodeengineclient/health.go @@ -0,0 +1,80 @@ +package nodeengineclient + +import ( + "encoding/json" + "fmt" + "time" + + seat "git.novox.be/novox/mesh-contracts/go/seats/nodeengine" + "git.novox.be/novox/mesh-sdk/go/keys" +) + +// A node's own health report (the state health, one key per node, written by that node's engine alone) and the data items +// a module declares as the engine placed them (the state data, `.`): their layouts are the seat's contract +// (seat.HealthByNodeValue, seat.DataByNodeModuleValue); this file is their codec, shared by the engine and its readers. + +// EncodeHealth is h as JSON at the current version; it refuses a report naming no node, and a fall back to the old login +// that names another node, no login fallen back from, no reason or no time. +func EncodeHealth(h seat.HealthByNodeValue) ([]byte, error) { + h.V = keys.Version + if h.Node == "" { + return nil, fmt.Errorf("node-engine: a node's health report names its node") + } + if f := h.Fallback; f != nil && (f.Node != h.Node || f.From == "" || f.Why == "" || f.At.IsZero()) { + return nil, fmt.Errorf("node-engine: a node's fall back to its old login names its node %s, the login fallen back from, why and when", h.Node) + } + return json.Marshal(h) +} + +// DecodeHealth reads a report; it refuses another version (keys.ErrUnknownVersion) and a field the contract does not know. +func DecodeHealth(raw []byte) (seat.HealthByNodeValue, error) { + if err := keys.CheckVersion(raw); err != nil { + return seat.HealthByNodeValue{}, err + } + h, err := seat.DecodeHealthByNodeValue(raw) + if err != nil { + return seat.HealthByNodeValue{}, fmt.Errorf("node-engine: a node's health report is not readable: %w", err) + } + return h, nil +} + +// SameHealth says two reports say the same, whenever each was written. +func SameHealth(a, b seat.HealthByNodeValue) bool { + a.At, b.At = time.Time{}, time.Time{} + return sameJSON(a, b) +} + +// DataKey is a module's key on a node in the state data, `.`, its module part escaped as a have's is. +func DataKey(node, module string) (string, error) { return HaveKey(node, module) } + +// DataNodeKeys is the pattern of every data key of a node, `.*`. +func DataNodeKeys(node string) (string, error) { + if err := keys.CheckName("node", node); err != nil { + return "", err + } + return node + ".*", nil +} + +// EncodeData is d as JSON at the current version; it refuses a node or module the key refuses. +func EncodeData(d seat.DataByNodeModuleValue) ([]byte, error) { + d.V = keys.Version + if _, err := DataKey(d.Node, d.Module); err != nil { + return nil, err + } + if d.Items == nil { + d.Items = []seat.PlacedDataItem{} + } + return json.Marshal(d) +} + +// DecodeData reads a module's data items; it refuses another version (keys.ErrUnknownVersion). +func DecodeData(raw []byte) (seat.DataByNodeModuleValue, error) { + if err := keys.CheckVersion(raw); err != nil { + return seat.DataByNodeModuleValue{}, err + } + d, err := seat.DecodeDataByNodeModuleValue(raw) + if err != nil { + return seat.DataByNodeModuleValue{}, fmt.Errorf("node-engine: a module's data items are not readable: %w", err) + } + return d, nil +} diff --git a/go/clients/nodeengineclient/keys.go b/go/clients/nodeengineclient/keys.go index edb9b0a..8876150 100644 --- a/go/clients/nodeengineclient/keys.go +++ b/go/clients/nodeengineclient/keys.go @@ -120,3 +120,11 @@ func ParseComposedKey(key string) (node, module, fingerprintPart string, isNode } return refused(fmt.Errorf("the key has %d parts", len(parts))) } + +// HaveNodeKey is the key, in the state have, of a node's own facts: `.node`. +func HaveNodeKey(node string) (string, error) { + if err := keys.CheckName("node", node); err != nil { + return "", err + } + return node + "." + NodeWord, nil +}