AM-C1: the operator-channel client puts a key prefix before every key of its own state

Client.KeyPrefix is put before the keys the client writes and reads in its own State; empty for every
asker today, so today's keys stay; Where still reads the router's record under <module>.<id>.

Builds: design 46 milestone 7 §10, §22 (design 47 G, approval mark)

Claude-Session: https://claude.ai/code/session_01JG73KEq4zoDfM9WtYpcQgZ
This commit is contained in:
2026-11-11 10:35:35 +01:00
parent f5d76e84ca
commit ada45be38b
2 changed files with 30 additions and 5 deletions
@@ -27,6 +27,9 @@ type Client struct {
Module string
// State is the module's own state the asks are kept in.
State Store
// KeyPrefix is put before every key of State the client writes and reads; empty for every asker today, so
// today's keys stay (design 46 milestone 7 §10). The router's record Where reads is never prefixed.
KeyPrefix string
// Now is the clock; nil is time.Now.
Now func() time.Time
// Publish, Record and Subscribe reach the bus; nil is the stdio SDK's, through the module's runtime.
@@ -63,7 +66,7 @@ type Kept struct {
Answer *seat.Warrant `json:"answer,omitempty"`
}
func key(id string) string { return "ask." + id }
func (c *Client) key(id string) string { return c.KeyPrefix + "ask." + id }
// Ask checks an ask, keeps it, and publishes it under the module's own name. An ask with an id already kept
// is refused: a new question is a new id.
@@ -71,7 +74,7 @@ func (c *Client) Ask(a seat.Ask) error {
if err := Check(a, c.now()); err != nil {
return err
}
if _, err := c.State.Create(key(a.ID), Kept{Ask: a, Asked: c.now()}); err != nil {
if _, err := c.State.Create(c.key(a.ID), Kept{Ask: a, Asked: c.now()}); err != nil {
if errors.Is(err, stdio.ErrChanged) {
return fmt.Errorf("an ask %s was already made: a new question is a new id", a.ID)
}
@@ -130,7 +133,7 @@ func (c *Client) Take(w seat.Warrant, act func(seat.Option, seat.Warrant) error)
if w.Asker != c.Module {
return Refused, fmt.Errorf("a warrant for %s reached %s", w.Asker, c.Module)
}
e, err := c.State.Get(key(w.Ask))
e, err := c.State.Get(c.key(w.Ask))
if err != nil {
return "", err
}
@@ -147,7 +150,7 @@ func (c *Client) Take(w seat.Warrant, act func(seat.Option, seat.Warrant) error)
if w.Outcome != OutcomeChosen {
k.Ended = w.Outcome
k.Answer = &w
if _, err := c.State.Update(key(w.Ask), k, e.Revision); err != nil && !errors.Is(err, stdio.ErrChanged) {
if _, err := c.State.Update(c.key(w.Ask), k, e.Revision); err != nil && !errors.Is(err, stdio.ErrChanged) {
return "", err
}
return Recorded, nil
@@ -158,7 +161,7 @@ func (c *Client) Take(w seat.Warrant, act func(seat.Option, seat.Warrant) error)
}
// Marked before acting: of two deliveries, or two instances, only the one whose write stands acts.
k.Acted, k.Ended, k.Answer = true, OutcomeChosen, &w
if _, err := c.State.Update(key(w.Ask), k, e.Revision); err != nil {
if _, err := c.State.Update(c.key(w.Ask), k, e.Revision); err != nil {
if errors.Is(err, stdio.ErrChanged) {
return Done, nil
}
@@ -139,3 +139,25 @@ func TestAnAskEndedWithoutAChoiceIsRecordedAndNothingDone(t *testing.T) {
t.Errorf("%s", h)
}
}
func TestAKeyPrefixIsPutBeforeEveryKeyOfTheClientsOwnStateAndNotBeforeWhere(t *testing.T) {
for _, tc := range []struct{ prefix, key string }{{"n1.", "n1.ask.d-1"}, {"", "ask.d-1"}} {
c, _ := client()
store := c.State.(*mem)
c.KeyPrefix = tc.prefix
var asked string
c.Record = func(k string) (*stdio.StateEntry, error) { asked = k; return nil, nil }
if err := c.Ask(askedHeld()); err != nil {
t.Fatal(err)
}
if _, ok := store.vals[tc.key]; len(store.vals) != 1 || !ok {
t.Fatalf("prefix %q: keys written %v, want %s", tc.prefix, store.vals, tc.key)
}
if h, err := c.Take(askedWarrant(), func(seat.Option, seat.Warrant) error { return nil }); h != Acted || err != nil {
t.Fatalf("prefix %q: the warrant was not read from %s: %s %v", tc.prefix, tc.key, h, err)
}
if _, _, err := c.Where("an-asker.d-1"); err != nil || asked != "an-asker.d-1" {
t.Errorf("prefix %q: Where read %q", tc.prefix, asked)
}
}
}