From 76e025dbbc35927b1884cd640408e9a18054d3c5 Mon Sep 17 00:00:00 2001 From: jochen Date: Wed, 4 Nov 2026 14:31:36 +0100 Subject: [PATCH] J2: a state read carries the time the bus wrote the value, as at (stdio.StateEntry.At) Claude-Session: https://claude.ai/code/session_01JG73KJNrH5EgFXMKKYSrig --- node-tools/internal/bus/per_node.go | 2 +- node-tools/internal/bus/state.go | 4 +++- node-tools/internal/bus/state_pernode_test.go | 3 ++- node-tools/internal/bus/state_watch_test.go | 2 +- 4 files changed, 7 insertions(+), 4 deletions(-) diff --git a/node-tools/internal/bus/per_node.go b/node-tools/internal/bus/per_node.go index ba64d35..dae0580 100644 --- a/node-tools/internal/bus/per_node.go +++ b/node-tools/internal/bus/per_node.go @@ -33,7 +33,7 @@ func (c *Conn) perNodeGet(s StateIssued, key string) (*StateEntry, error) { if err != nil || e == nil { return nil, err } - return &StateEntry{Key: e.Key(), Value: valueOf(e.Value()), Revision: e.Revision()}, nil + return &StateEntry{Key: e.Key(), Value: valueOf(e.Value()), Revision: e.Revision(), At: e.Created().UTC()}, nil } func (c *Conn) perNodeKeys(s StateIssued) ([]string, error) { diff --git a/node-tools/internal/bus/state.go b/node-tools/internal/bus/state.go index 935ddff..4e53368 100644 --- a/node-tools/internal/bus/state.go +++ b/node-tools/internal/bus/state.go @@ -39,6 +39,8 @@ type StateEntry struct { Key string `json:"key"` Value json.RawMessage `json:"value"` Revision uint64 `json:"revision"` + // At is when the bus wrote this value (the entry's created time), as stdio.StateEntry.At reads it. + At time.Time `json:"at,omitzero"` } // StateChange is one change a watch delivers: a key put, deleted, or expired (its own time to live, or the @@ -134,7 +136,7 @@ func (c *Conn) StateGet(module, name, key string) (*StateEntry, error) { if err != nil { return nil, err } - return &StateEntry{Key: e.Key(), Value: valueOf(e.Value()), Revision: e.Revision()}, nil + return &StateEntry{Key: e.Key(), Value: valueOf(e.Value()), Revision: e.Revision(), At: e.Created().UTC()}, nil } // StateKept is one value a bucket still keeps for a key: a put, or a delete (a purge reads as one). diff --git a/node-tools/internal/bus/state_pernode_test.go b/node-tools/internal/bus/state_pernode_test.go index de04809..82baf52 100644 --- a/node-tools/internal/bus/state_pernode_test.go +++ b/node-tools/internal/bus/state_pernode_test.go @@ -10,6 +10,7 @@ import ( "strings" "sync" "testing" + "time" "github.com/novox/mesh-tools/node-tools/internal/bus" mt "github.com/novox/mesh-tools/node-tools/internal/meshtest" @@ -47,7 +48,7 @@ func TestAPerNodeStateIsServedAcrossEveryNodesBucket(t *testing.T) { return nil }) - if e, err := conn.StateGet("mesh-provisioner", "mesh-host.composed", "b.m2.f2"); err != nil || e == nil || e.Key != "b.m2.f2" { + if e, err := conn.StateGet("mesh-provisioner", "mesh-host.composed", "b.m2.f2"); err != nil || e == nil || e.Key != "b.m2.f2" || e.At.IsZero() || time.Since(e.At) > time.Minute { t.Fatalf("get from the second node's bucket: %+v, %v", e, err) } if e, err := conn.StateGet("mesh-provisioner", "mesh-host.composed", "q.m.f"); err != nil || e != nil { diff --git a/node-tools/internal/bus/state_watch_test.go b/node-tools/internal/bus/state_watch_test.go index cb69445..b4a40a7 100644 --- a/node-tools/internal/bus/state_watch_test.go +++ b/node-tools/internal/bus/state_watch_test.go @@ -151,7 +151,7 @@ func TestAMembershipTheGrantsPartWroteWrappedIsServed(t *testing.T) { if _, err := conn.StatePut("gitea", "repos", "novox.hq", []byte(`{"v":1}`)); err != nil { t.Fatalf("the state the membership issues is refused: %v", err) } - if e, err := conn.StateGet("gitea", "repos", "novox.hq"); err != nil || e == nil || string(e.Value) != `{"v":1}` { + if e, err := conn.StateGet("gitea", "repos", "novox.hq"); err != nil || e == nil || string(e.Value) != `{"v":1}` || e.At.IsZero() || time.Since(e.At) > time.Minute { t.Errorf("the state reads back %+v, %v", e, err) } conn.Follow("pages")