From 276412d7ffe5bc0f765aad1d77e5d557652287fe Mon Sep 17 00:00:00 2001 From: jochen Date: Wed, 11 Nov 2026 13:54:40 +0100 Subject: [PATCH 1/3] 1966 subtask 1: a chunk 0 again starts a part afresh in parts.Receiver.Take A part the holder lacks travels again whole from chunk 0, so its earlier chunks are dropped and chunk 0 is taken; any other chunk that is not the cursor is still refused, naming the cursor (1966/AC203-1, AC203-2). Builds: design 51 Publishing, the bullet "The file travels in parts through the seat" Claude-Session: https://claude.ai/code/session_01JG73KEq4zoDfM9WtYpcQgZ --- sdk/parts/receive.go | 9 +++++++-- sdk/parts/receive_test.go | 7 ++++++- 2 files changed, 13 insertions(+), 3 deletions(-) diff --git a/sdk/parts/receive.go b/sdk/parts/receive.go index c77bcfe0..9fac2795 100644 --- a/sdk/parts/receive.go +++ b/sdk/parts/receive.go @@ -239,7 +239,7 @@ func (r *Receiver[M]) arm(id string) error { } // Take takes one chunk of a part from the transfer's opener: chunks of a part arrive in sequence from 0, the last -// marked; the whole part must hash to its digest, else its chunks are dropped and the part is lacked again. Belongs, +// marked, and a chunk 0 again starts the part afresh (its earlier chunks are dropped); the whole part must hash to its digest, else its chunks are dropped and the part is lacked again. Belongs, // when given, refuses a transfer the caller's own shape says is not this call's (another seat's, say). When the last // lacked part arrives the transfer ends: the caller's check, then its store. func (r *Receiver[M]) Take(id, opener, digest string, sequence int, last bool, data []byte, belongs func(Transfer[M]) error) (Received, error) { @@ -264,9 +264,14 @@ func (r *Receiver[M]) Take(id, opener, digest string, sequence int, last bool, d return Received{}, fmt.Errorf("transfer %s names no part %s", id, digest) case slices.Contains(t.Held, digest): return Received{ID: id, Lacks: t.Lacks()}, nil - case sequence != t.Chunks[digest]: + case sequence != t.Chunks[digest] && sequence != 0: return Received{}, fmt.Errorf("part %s of transfer %s waits for chunk %d, not %d", digest, id, t.Chunks[digest], sequence) } + if sequence == 0 && t.Chunks[digest] != 0 { + // A part the holder lacks travels again whole from chunk 0 (design 51 Publishing): its earlier chunks are dropped. + r.dropChunks(t, digest) + t.Chunks[digest] = 0 + } if err := r.Objects.Put(r.chunkName(id, digest, sequence), data); err != nil { return Received{}, fmt.Errorf("the objects did not take the chunk: %w", err) } diff --git a/sdk/parts/receive_test.go b/sdk/parts/receive_test.go index d4ee5dfb..54046826 100644 --- a/sdk/parts/receive_test.go +++ b/sdk/parts/receive_test.go @@ -133,9 +133,14 @@ func aHolderTakesAFileInParts(t *testing.T) { if _, err := pkg.Open("t1", "other", []Want{part}, packageShape{}); err == nil || !strings.Contains(err.Error(), "another caller") { t.Errorf("reopen by another = %v", err) } - if _, err := pkg.Take("t1", "builder", part.Digest, 0, false, blob[:1000], nil); err == nil || !strings.Contains(err.Error(), "waits for chunk 1") { + // 1966/AC203-2: a chunk that is neither the cursor nor 0 is refused, naming the cursor. + if _, err := pkg.Take("t1", "builder", part.Digest, 2, false, blob[:1000], nil); err == nil || !strings.Contains(err.Error(), "waits for chunk 1, not 2") { t.Errorf("a chunk out of sequence = %v", err) } + // 1966/AC203-1: a chunk 0 again (the part lacked travels whole again) drops the earlier chunks and takes it. + if _, err := pkg.Take("t1", "builder", part.Digest, 0, false, blob[:1000], nil); err != nil { + t.Errorf("chunk 0 again = %v", err) + } a, err = pkg.Take("t1", "builder", part.Digest, 1, true, blob[1000:], nil) if err != nil || !a.Ended || len(pkgLog) != 1 || pkgLog[0].outcome != "stored" || string(pkgLog[0].parts[part.Digest]) != string(blob) { t.Fatalf("pkg end = %+v, %v, %+v", a, err, pkgLog) From ca9aad9757f629b8f13bdd365502dc074607b4a3 Mon Sep 17 00:00:00 2001 From: jochen Date: Wed, 11 Nov 2026 13:58:23 +0100 Subject: [PATCH 2/3] 1966 subtask 2: the grants part composes a binding from an answer of mesh-broker's list without binding MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit An answer no entry of which carries binding gets, for a key-value or object-store entry whose settings hold the bus object its bus-name names, the binding composed from the answer's own fields; a stream, a ledger and an entry not held stay unbound and are logged once per answer; grants_first_views shows composed and unbound (1966/AC203-3..n). An answer where any entry carries binding is applied by §4 alone. Builds: design 25 bindings-first-view §9, §4, §6, §7, §8 Claude-Session: https://claude.ai/code/session_01JG73KEq4zoDfM9WtYpcQgZ --- .../cmd/mesh-grants/integration_test.go | 28 ++++++ modules/mesh-grants/grants_test.go | 78 +++++++++++----- .../internal/grants/bindingsview.go | 89 ++++++++++++++----- 3 files changed, 153 insertions(+), 42 deletions(-) diff --git a/modules/mesh-grants/cmd/mesh-grants/integration_test.go b/modules/mesh-grants/cmd/mesh-grants/integration_test.go index e438366b..b48e8e51 100644 --- a/modules/mesh-grants/cmd/mesh-grants/integration_test.go +++ b/modules/mesh-grants/cmd/mesh-grants/integration_test.go @@ -224,6 +224,8 @@ func TestTheGrantsPartWhole(t *testing.T) { type listGate struct{ asked, release chan struct{} } var gate atomic.Pointer[listGate] var answeredGated, listAsked, listAnswered atomic.Int32 + // While set, the bus module answers as a build from before the field `binding` (design 25 bindings-first-view §9). + var oldBuild atomic.Bool for _, subject := range []string{"mesh.seat.mesh-broker.call.list.>", "mesh.seat.mesh-broker.tool.list"} { if _, err := nc.Subscribe(subject, func(m *nats.Msg) { if !listing.Load() { @@ -233,6 +235,15 @@ func TestTheGrantsPartWhole(t *testing.T) { listAsked.Add(1) defer listAnswered.Add(1) var more []any + if oldBuild.Load() { + answer(m, map[string]any{"entries": []any{ + map[string]any{"module": "mod-c", "local-name": "late", "storage-type": "key-value", "template": "state", "bus-name": "mod-c_late", + "settings": map[string]any{"KV_mod-c_late": map[string]any{}}}, + map[string]any{"module": "mod-c", "local-name": "feed", "storage-type": "stream", "bus-name": "mod-c_feed", + "settings": map[string]any{"mod-c_feed": map[string]any{}}}, + }}) + return + } if g := gate.Load(); g != nil { g.asked <- struct{}{} <-g.release @@ -440,6 +451,23 @@ func TestTheGrantsPartWhole(t *testing.T) { if b := bindingOf(); len(b) != 0 { t.Fatalf("after an answer that lists a binding dropped since its ask: %+v", b) } + // 1966/AC203-3..: a bus module's build from before the field answers no binding on any entry (design 25 bindings-first-view + // §9): the key-value entry the bus holds gets its binding composed from the answer's fields, the stream is left unbound. + oldBuild.Store(true) + askedAgain, _ = json.Marshal(grants.Ask{By: "someone@desk", At: time.Now().Add(2 * time.Second), FirstView: grants.FirstViewBindings}) + if _, err := kvs[grants.Module+"."+grants.StateUserList].Put(ctx, grants.KeyAsked, askedAgain); err != nil { + t.Fatal(err) + } + until("the binding of the entry composed from an answer with no binding, the stream left unbound", func() bool { + e, err := kvs[grants.Module+"."+grants.StateUserList].Get(ctx, grants.KeyFirstViews) + if err != nil { + return false + } + var st grants.FirstViewsState + b := bindingOf() + return json.Unmarshal(e.Value(), &st) == nil && st.Bindings != nil && st.Bindings.Composed == 1 && + slices.Equal(st.Bindings.Unbound, []string{"mod-c/feed (stream)"}) && len(b) == 1 && b[0].Bucket == "mod-c_late" + }) for _, bucket := range []string{"mesh-controller_lease", grants.Module + "_lease", grants.Module + "_takeover"} { if _, err := js.KeyValue(ctx, bucket); !errors.Is(err, jetstream.ErrBucketNotFound) { t.Errorf("the bucket %s: %v, want none", bucket, err) diff --git a/modules/mesh-grants/grants_test.go b/modules/mesh-grants/grants_test.go index 601792c7..ef3fad7c 100644 --- a/modules/mesh-grants/grants_test.go +++ b/modules/mesh-grants/grants_test.go @@ -3064,8 +3064,16 @@ func TestGrants(t *testing.T) { "binding": map[string]any{"local": local, "storage": "key-value", "template": "state", "bucket": bucket}} } withoutBinding := map[string]any{"module": "mod-x", "local-name": "waiting", "storage-type": "key-value", "bus-name": "mod-x_waiting", "state": "waiting"} - oldBusModule := map[string]any{"module": "mod-x", "local-name": "late", "storage-type": "key-value", "bus-name": "mod-x_late", - "settings": map[string]any{"mod-x_late": map[string]any{}}} + // Design 25 bindings-first-view §9: the entries of a bus module's answer from before the field, with no binding. An + // entry carries what the bus holds in settings, keyed by the bus object's name (KV_ and OBJ_ before the bus name). + noBinding := func(local, storage, object string, held bool) map[string]any { + e := map[string]any{"module": "mod-x", "local-name": local, "storage-type": storage, "template": "state", "bus-name": "mod-x_" + local} + if held { + e["settings"] = map[string]any{object + "mod-x_" + local: map[string]any{}} + } + return e + } + oldBusModule := noBinding("late", "key-value", "KV_", true) // A bus module's setup: the part with the first view, the conditions it said, the bindings it keeps for mod-x. type setup struct { b *fakeBus @@ -3109,7 +3117,7 @@ func TestGrants(t *testing.T) { var bound grants.Bound _ = json.Unmarshal(raw, &bound) for local, b := range bound.Bindings { - out[local] = b.Bucket + out[local] = b.Bucket + b.ObjectStore } } return out @@ -3122,17 +3130,21 @@ func TestGrants(t *testing.T) { } } for _, row := range []struct { - name string - held map[string]string // kept before, by local name - answer []any - during func(*setup) // heard while the ask runs - want map[string]string - status [3]int // written, rewritten, listed - said map[string]string - legacy bool - again []any // a second answer - wantAgn map[string]string - saidAgn map[string]string + name string + held map[string]string // kept before, by local name + answer []any + during func(*setup) // heard while the ask runs + want map[string]string + status [3]int // written, rewritten, listed + said map[string]string + // 1966/AC203-3..: §9's composed count and unbound entries after the last answer, and how many answers said + // that they carry no binding on any entry. + composed int + unbound []string + noted int + again []any // a second answer + wantAgn map[string]string + saidAgn map[string]string }{ {name: "a binding the events never brought is written", answer: []any{entry("late", "mod-x_late")}, want: map[string]string{"late": "mod-x_late"}, status: [3]int{1, 0, 1}}, {name: "the same binding writes nothing", held: map[string]string{"late": "mod-x_late"}, answer: []any{entry("late", "mod-x_late")}, @@ -3140,7 +3152,7 @@ func TestGrants(t *testing.T) { {name: "a different binding is rewritten", held: map[string]string{"late": "mod-x_old"}, answer: []any{entry("late", "mod-x_late")}, want: map[string]string{"late": "mod-x_late"}, status: [3]int{0, 1, 1}}, {name: "an entry listed without a binding changes nothing", held: map[string]string{"waiting": "mod-x_waiting"}, answer: []any{withoutBinding}, - want: map[string]string{"waiting": "mod-x_waiting"}, status: [3]int{0, 0, 1}}, + want: map[string]string{"waiting": "mod-x_waiting"}, status: [3]int{0, 0, 1}, unbound: []string{"mod-x/waiting (key-value)"}, noted: 1}, {name: "a binding the answer omits is kept, raises its condition, and a later answer that lists it clears it", held: map[string]string{"gone": "mod-x_gone"}, answer: []any{}, want: map[string]string{"gone": "mod-x_gone"}, said: map[string]string{grants.BindingUnlisted + ".mod-x.gone": "raised"}, @@ -3151,8 +3163,26 @@ func TestGrants(t *testing.T) { {name: "a dropped heard since the ask is not written back, and raises nothing", held: map[string]string{"late": "mod-x_late"}, answer: []any{entry("late", "mod-x_late")}, during: func(st *setup) { hear(st, "dropped", "late", "") }, want: map[string]string{}, status: [3]int{0, 0, 1}, said: map[string]string{}}, - {name: "an answer of a bus module from before the field writes nothing and says so once", answer: []any{oldBusModule}, - want: map[string]string{}, status: [3]int{0, 0, 1}, legacy: true, again: []any{oldBusModule}, wantAgn: map[string]string{}}, + // §9 (1966/AC203-3..): an answer no entry of which carries a binding. + {name: "§9 a key-value entry the bus holds is composed from the answer's fields, and said once per answer", answer: []any{oldBusModule}, + want: map[string]string{"late": "mod-x_late"}, status: [3]int{1, 0, 1}, composed: 1, noted: 2, again: []any{oldBusModule}, + wantAgn: map[string]string{"late": "mod-x_late"}}, + {name: "§9 an object-store entry is composed with its object store", answer: []any{noBinding("media", "object-store", "OBJ_", true)}, + want: map[string]string{"media": "mod-x_media"}, status: [3]int{1, 0, 1}, composed: 1, noted: 1}, + {name: "§9 a stream and a ledger are left unbound and logged", answer: []any{noBinding("s", "stream", "", true), noBinding("l", "ledger", "", true)}, + want: map[string]string{}, status: [3]int{0, 0, 2}, unbound: []string{"mod-x/l (ledger)", "mod-x/s (stream)"}, noted: 1}, + {name: "§9 an entry without settings is left unbound and logged", answer: []any{noBinding("waiting", "key-value", "KV_", false)}, + want: map[string]string{}, status: [3]int{0, 0, 1}, unbound: []string{"mod-x/waiting (key-value)"}, noted: 1}, + {name: "§9 an entry whose settings lack its bus object is left unbound and logged", + answer: []any{func() map[string]any { + e := noBinding("late", "key-value", "KV_", true) + e["settings"] = map[string]any{"mod-x_late": map[string]any{}} + return e + }()}, + want: map[string]string{}, status: [3]int{0, 0, 1}, unbound: []string{"mod-x/late (key-value)"}, noted: 1}, + {name: "§9 an answer where any entry carries a binding is applied by §4 alone, nothing composed", + answer: []any{entry("other", "mod-x_other"), oldBusModule}, + want: map[string]string{"other": "mod-x_other"}, status: [3]int{1, 0, 2}}, } { t.Run(row.name, func(t *testing.T) { st := newSetup() @@ -3181,7 +3211,7 @@ func TestGrants(t *testing.T) { t.Fatal(err) } check(row.want, row.said) - if s := st.v.Status(); [3]int{s.Written, s.Rewritten, s.Listed} != row.status && !row.legacy && row.again == nil { + if s := st.v.Status(); [3]int{s.Written, s.Rewritten, s.Listed} != row.status && row.again == nil { t.Errorf("status wrote %d, rewrote %d, listed %d; want %v", s.Written, s.Rewritten, s.Listed, row.status) } if row.again != nil { @@ -3194,8 +3224,16 @@ func TestGrants(t *testing.T) { t.Errorf("by hand %d, want 1", st.v.Status().ByHand) } } - if told := strings.Count(strings.Join(st.logs, "\n"), "no binding (a bus module from before the field)"); row.legacy && told != 1 || !row.legacy && told != 0 { - t.Errorf("a bus module from before the field was told %d times", told) + if told := strings.Count(strings.Join(st.logs, "\n"), "answered with no binding on any entry"); told != row.noted { + t.Errorf("an answer with no binding was told %d times, want %d", told, row.noted) + } + if s := st.v.Status(); s.Composed != row.composed || !slices.Equal(s.Unbound, row.unbound) { + t.Errorf("status composed %d, unbound %v; want %d, %v", s.Composed, s.Unbound, row.composed, row.unbound) + } + for _, u := range row.unbound { + if !strings.Contains(strings.Join(st.logs, "\n"), u) { + t.Errorf("the log does not name the unbound entry %s: %v", u, st.logs) + } } }) } diff --git a/modules/mesh-grants/internal/grants/bindingsview.go b/modules/mesh-grants/internal/grants/bindingsview.go index e596c596..e75eacf0 100644 --- a/modules/mesh-grants/internal/grants/bindingsview.go +++ b/modules/mesh-grants/internal/grants/bindingsview.go @@ -43,6 +43,11 @@ type BindingsStatus struct { Listed int `json:"listed"` Written int `json:"written"` Rewritten int `json:"rewritten"` + // Composed is how many entries of the last answer had their binding composed from the answer's own fields, and Unbound + // the entries it left unbound, `/ ()`: both only for an answer no entry of which + // carries `binding` (§9). + Composed int `json:"composed"` + Unbound []string `json:"unbound,omitempty"` // Unlisted are the bindings the part holds that the last answer did not list, `/`. Unlisted []string `json:"unlisted,omitempty"` Asks int `json:"asks"` @@ -77,7 +82,6 @@ type BindingsView struct { held []heldEvent touched map[string]bool raised map[string]bool // the unlisted conditions raised, by `/` - legacy bool } type heldEvent struct { @@ -323,27 +327,20 @@ func (v *BindingsView) apply(p *Part, raw json.RawMessage) error { if err := json.Unmarshal(raw, &answer); err != nil { return fmt.Errorf("%s answered what is not a list: %w", CallBrokerList, err) } + entries, composed, unbound := v.compose(answer.Entries) p.bindMu.Lock() - written, rewritten, holdsNoBinding, err := v.take(p, answer.Entries) + written, rewritten, err := v.take(p, entries, composed) p.bindMu.Unlock() if err != nil { return err } - if holdsNoBinding > 0 { - v.mu.Lock() - said := v.legacy - v.legacy = true - v.mu.Unlock() - if !said { - v.logf("the seat %s's verb %s lists %d entries whose objects the bus holds with no binding (a bus module from before the field): nothing is written from the answer, and the events are heard as before", BrokerSeat, BrokerListVerb, holdsNoBinding) - } - } v.mu.Lock() wasRaised := v.st.Raised v.st.Raised, v.st.Error = false, "" v.st.Answered = v.now() v.st.Listed = len(answer.Entries) v.st.Written, v.st.Rewritten = written, rewritten + v.st.Composed, v.st.Unbound = len(composed), unbound status := v.statusLocked() v.mu.Unlock() if wasRaised { @@ -354,10 +351,57 @@ func (v *BindingsView) apply(p *Part, raw json.RawMessage) error { return nil } +// compose gives the entries of an answer that carries no `binding` on any entry (a bus module's build from before the +// field) the binding its own fields name whole (design 25 bindings-first-view §9): for a key-value entry or an object-store +// entry whose settings hold the bus object its bus name names. It answers the entries with those bindings, the entries +// composed (`/`) and the ones left unbound, and says once what it did. An answer where any entry carries +// `binding` is applied by §4 alone: it answers the entries as they are. +func (v *BindingsView) compose(entries []meshbroker.Held) ([]meshbroker.Held, map[string]bool, []string) { + if len(entries) == 0 || slices.ContainsFunc(entries, func(e meshbroker.Held) bool { return e.Binding != nil }) { + return entries, nil, nil + } + out := slices.Clone(entries) + composed := map[string]bool{} + var unbound []string + for i, e := range out { + if b, ok := composeBinding(e); ok { + out[i].Binding = b + composed[localKey(e.Module, e.LocalName)] = true + continue + } + unbound = append(unbound, fmt.Sprintf("%s (%s)", localKey(e.Module, e.LocalName), e.StorageType)) + } + sort.Strings(unbound) + v.logf("the seat %s's verb %s answered with no binding on any entry (a bus module from before the field): %d bindings composed from its fields, left unbound: %s", + BrokerSeat, BrokerListVerb, len(composed), strings.Join(unbound, ", ")) + return out, composed, unbound +} + +// composeBinding is §9's binding of one entry: only for the storage types whose binding the answer names whole, and only +// when the entry's settings hold the bus object its bus name names (KV_ and the bus name, OBJ_ and the bus name). +func composeBinding(e meshbroker.Held) (*meshbroker.Binding, bool) { + b := &meshbroker.Binding{Local: e.LocalName, Storage: e.StorageType, Template: e.Template} + switch e.StorageType { + case "key-value": + if _, held := e.Settings["KV_"+e.BusName]; !held { + return nil, false + } + b.Bucket = e.BusName + case "object-store": + if _, held := e.Settings["OBJ_"+e.BusName]; !held { + return nil, false + } + b.ObjectStore = e.BusName + default: + return nil, false + } + return b, e.LocalName != "" && e.BusName != "" +} + // take writes the bindings of the entries an event did not name since the ask was sent, keeps every binding the answer does -// not list and raises its unlisted condition, and answers how many bindings it wrote and rewrote, and how many entries it -// lists with their objects held and no binding. The caller holds the part's bindMu. -func (v *BindingsView) take(p *Part, entries []meshbroker.Held) (written, rewritten, noBinding int, err error) { +// not list and raises its unlisted condition, and answers how many bindings it wrote and rewrote; composed names the +// entries whose binding compose gave them. The caller holds the part's bindMu. +func (v *BindingsView) take(p *Part, entries []meshbroker.Held, composed map[string]bool) (written, rewritten int, err error) { v.mu.Lock() touched := copyOf(v.touched) v.mu.Unlock() @@ -366,9 +410,6 @@ func (v *BindingsView) take(p *Part, entries []meshbroker.Held) (written, rewrit for _, e := range entries { listed[localKey(e.Module, e.LocalName)] = true if e.Binding == nil { - if len(e.Settings) > 0 { - noBinding++ - } continue } byModule[e.Module] = append(byModule[e.Module], e) @@ -384,7 +425,7 @@ func (v *BindingsView) take(p *Part, entries []meshbroker.Held) (written, rewrit } b, berr := p.bound(module) if berr != nil { - return written, rewritten, noBinding, berr + return written, rewritten, berr } changed := false for _, e := range byModule[module] { @@ -396,6 +437,10 @@ func (v *BindingsView) take(p *Part, entries []meshbroker.Held) (written, rewrit v.logf("the binding of %s %s in the answer of %s does not read: not written", module, e.LocalName, CallBrokerList) continue } + cause := "the first view of the bindings (" + CallBrokerList + ")" + if composed[localKey(module, e.LocalName)] { + cause = "the first view of the bindings, composed from " + CallBrokerList + "'s fields" + } was, had := b.Bindings[e.LocalName] if had && sameBinding(was, bind) { continue @@ -404,20 +449,20 @@ func (v *BindingsView) take(p *Part, entries []meshbroker.Held) (written, rewrit changed = true if had { rewritten++ - v.logf("the binding of %s %s was rewritten, the cause the first view of the bindings (%s)", module, e.LocalName, CallBrokerList) + v.logf("the binding of %s %s was rewritten, the cause %s", module, e.LocalName, cause) } else { written++ - v.logf("the binding of %s %s was written, the cause the first view of the bindings (%s)", module, e.LocalName, CallBrokerList) + v.logf("the binding of %s %s was written, the cause %s", module, e.LocalName, cause) } } if changed { raw, _ := json.Marshal(b) if _, perr := p.Store.Put(StateMemberships, BoundKey(module), raw); perr != nil { - return written, rewritten, noBinding, fmt.Errorf("its own %s %s cannot be written: %w", StateMemberships, BoundKey(module), perr) + return written, rewritten, fmt.Errorf("its own %s %s cannot be written: %w", StateMemberships, BoundKey(module), perr) } } } - return written, rewritten, noBinding, v.unlisted(p, listed, touched) + return written, rewritten, v.unlisted(p, listed, touched) } // unlisted keeps every binding the answer does not list and raises its condition, one per binding; a binding it lists again From 7db5b0dfe4794e990a989a8db8b40854dfe335a0 Mon Sep 17 00:00:00 2001 From: jochen Date: Wed, 11 Nov 2026 14:12:25 +0100 Subject: [PATCH 3/3] 1966 review round 1: Composed counts the bindings written or rewritten from fields; wrap Take's comment MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Builds: design 25 bindings-first-view §6, §8, §9 Claude-Session: https://claude.ai/code/session_01JG73KEq4zoDfM9WtYpcQgZ --- modules/mesh-grants/grants_test.go | 5 ++++- .../mesh-grants/internal/grants/bindingsview.go | 17 ++++++++++------- sdk/parts/receive.go | 4 ++-- 3 files changed, 16 insertions(+), 10 deletions(-) diff --git a/modules/mesh-grants/grants_test.go b/modules/mesh-grants/grants_test.go index ef3fad7c..74f7bacb 100644 --- a/modules/mesh-grants/grants_test.go +++ b/modules/mesh-grants/grants_test.go @@ -3165,8 +3165,11 @@ func TestGrants(t *testing.T) { want: map[string]string{}, status: [3]int{0, 0, 1}, said: map[string]string{}}, // §9 (1966/AC203-3..): an answer no entry of which carries a binding. {name: "§9 a key-value entry the bus holds is composed from the answer's fields, and said once per answer", answer: []any{oldBusModule}, - want: map[string]string{"late": "mod-x_late"}, status: [3]int{1, 0, 1}, composed: 1, noted: 2, again: []any{oldBusModule}, + want: map[string]string{"late": "mod-x_late"}, status: [3]int{1, 0, 1}, composed: 0, noted: 2, again: []any{oldBusModule}, wantAgn: map[string]string{"late": "mod-x_late"}}, + {name: "§9 an entry an event named since the ask is not written, so it is not counted composed", answer: []any{oldBusModule}, + during: func(st *setup) { hear(st, "created", "late", "mod-x_event") }, want: map[string]string{"late": "mod-x_event"}, + status: [3]int{0, 0, 1}, noted: 1}, {name: "§9 an object-store entry is composed with its object store", answer: []any{noBinding("media", "object-store", "OBJ_", true)}, want: map[string]string{"media": "mod-x_media"}, status: [3]int{1, 0, 1}, composed: 1, noted: 1}, {name: "§9 a stream and a ledger are left unbound and logged", answer: []any{noBinding("s", "stream", "", true), noBinding("l", "ledger", "", true)}, diff --git a/modules/mesh-grants/internal/grants/bindingsview.go b/modules/mesh-grants/internal/grants/bindingsview.go index e75eacf0..7b26c4aa 100644 --- a/modules/mesh-grants/internal/grants/bindingsview.go +++ b/modules/mesh-grants/internal/grants/bindingsview.go @@ -43,7 +43,7 @@ type BindingsStatus struct { Listed int `json:"listed"` Written int `json:"written"` Rewritten int `json:"rewritten"` - // Composed is how many entries of the last answer had their binding composed from the answer's own fields, and Unbound + // Composed is how many of the bindings the last answer wrote or rewrote were composed from its own fields, and Unbound // the entries it left unbound, `/ ()`: both only for an answer no entry of which // carries `binding` (§9). Composed int `json:"composed"` @@ -329,7 +329,7 @@ func (v *BindingsView) apply(p *Part, raw json.RawMessage) error { } entries, composed, unbound := v.compose(answer.Entries) p.bindMu.Lock() - written, rewritten, err := v.take(p, entries, composed) + written, rewritten, fromFields, err := v.take(p, entries, composed) p.bindMu.Unlock() if err != nil { return err @@ -340,7 +340,7 @@ func (v *BindingsView) apply(p *Part, raw json.RawMessage) error { v.st.Answered = v.now() v.st.Listed = len(answer.Entries) v.st.Written, v.st.Rewritten = written, rewritten - v.st.Composed, v.st.Unbound = len(composed), unbound + v.st.Composed, v.st.Unbound = fromFields, unbound status := v.statusLocked() v.mu.Unlock() if wasRaised { @@ -401,7 +401,7 @@ func composeBinding(e meshbroker.Held) (*meshbroker.Binding, bool) { // take writes the bindings of the entries an event did not name since the ask was sent, keeps every binding the answer does // not list and raises its unlisted condition, and answers how many bindings it wrote and rewrote; composed names the // entries whose binding compose gave them. The caller holds the part's bindMu. -func (v *BindingsView) take(p *Part, entries []meshbroker.Held, composed map[string]bool) (written, rewritten int, err error) { +func (v *BindingsView) take(p *Part, entries []meshbroker.Held, composed map[string]bool) (written, rewritten, fromFields int, err error) { v.mu.Lock() touched := copyOf(v.touched) v.mu.Unlock() @@ -425,7 +425,7 @@ func (v *BindingsView) take(p *Part, entries []meshbroker.Held, composed map[str } b, berr := p.bound(module) if berr != nil { - return written, rewritten, berr + return written, rewritten, fromFields, berr } changed := false for _, e := range byModule[module] { @@ -447,6 +447,9 @@ func (v *BindingsView) take(p *Part, entries []meshbroker.Held, composed map[str } b.Bindings[e.LocalName] = bind changed = true + if composed[localKey(module, e.LocalName)] { + fromFields++ + } if had { rewritten++ v.logf("the binding of %s %s was rewritten, the cause %s", module, e.LocalName, cause) @@ -458,11 +461,11 @@ func (v *BindingsView) take(p *Part, entries []meshbroker.Held, composed map[str if changed { raw, _ := json.Marshal(b) if _, perr := p.Store.Put(StateMemberships, BoundKey(module), raw); perr != nil { - return written, rewritten, fmt.Errorf("its own %s %s cannot be written: %w", StateMemberships, BoundKey(module), perr) + return written, rewritten, fromFields, fmt.Errorf("its own %s %s cannot be written: %w", StateMemberships, BoundKey(module), perr) } } } - return written, rewritten, v.unlisted(p, listed, touched) + return written, rewritten, fromFields, v.unlisted(p, listed, touched) } // unlisted keeps every binding the answer does not list and raises its condition, one per binding; a binding it lists again diff --git a/sdk/parts/receive.go b/sdk/parts/receive.go index 9fac2795..0eef245e 100644 --- a/sdk/parts/receive.go +++ b/sdk/parts/receive.go @@ -239,8 +239,8 @@ func (r *Receiver[M]) arm(id string) error { } // Take takes one chunk of a part from the transfer's opener: chunks of a part arrive in sequence from 0, the last -// marked, and a chunk 0 again starts the part afresh (its earlier chunks are dropped); the whole part must hash to its digest, else its chunks are dropped and the part is lacked again. Belongs, -// when given, refuses a transfer the caller's own shape says is not this call's (another seat's, say). When the last +// marked, and a chunk 0 again starts the part afresh (its earlier chunks are dropped); the whole part must hash to its +// digest, else its chunks are dropped and the part is lacked again. Belongs, when given, refuses a transfer the caller's own shape says is not this call's (another seat's, say). When the last // lacked part arrives the transfer ends: the caller's check, then its store. func (r *Receiver[M]) Take(id, opener, digest string, sequence int, last bool, data []byte, belongs func(Transfer[M]) error) (Received, error) { t, found, err := r.Transfers.Get(id)