Files
mesh-delivery/modules/mesh-grants/internal/grants/part.go
T
jschoubben c2e7249897 Keep every binding the bus module says created, and never acknowledge one not kept
distribution's membership carried 4 of its 8 bindings and mesh-knowledge's 3
of 6 (issue 1966). The grants part wrote a module's kept bindings with a blind
put, so two created heard at once lost one, and acknowledged every seat event
whatever its handler answered, while the log said it would come again. The
bindings are now written at the revision read and read again once on a
conflict, and an event not applied is left unacknowledged (a delayed nak).

Claude-Session: https://claude.ai/code/session_01NcbgfFSnn24M3GcqttoU7i
2026-11-09 14:01:41 +01:00

614 lines
26 KiB
Go

package grants
import (
"bytes"
"context"
"crypto/sha256"
"encoding/hex"
"encoding/json"
"fmt"
"git.novox.be/novox/mesh-contracts/go/seats/meshbroker"
"slices"
"sort"
"strings"
"sync"
"time"
"git.novox.be/novox/mesh-contracts/go/seats/nodeengine"
oc "git.novox.be/novox/mesh-contracts/operator-and-conversation/go/seats/operatorchannel"
"git.novox.be/novox/mesh-delivery/modules/mesh-grants/grantsstate"
"git.novox.be/novox/mesh-sdk/go/membership"
"mesh-grants/internal/buscompose"
)
// The part's own states, as its manifest declares them, and the keys of the two that hold one value.
const (
StateUserList = "user-list"
StateMemberships = "memberships"
StateAssertions = "assertions"
// StateGrants is the part's grants by module and fingerprint, the key layout of the controller's (grants.go).
StateGrants = grantsstate.PartGrantsState
KeyList = "list"
// KeyPass is the last pass's result, and KeyAsked a tool's ask to compose again now, both in StateUserList.
KeyPass = "pass"
KeyAsked = "asked"
)
// States is every state the part keeps.
var States = []string{StateAccounts, StateAssertions, StateGrants, StateMemberships, StateUserList, StateWithheld}
// The events the part says.
const (
EventUserList = "user-list-changed"
EventMembership = "membership-changed"
// EventGrantWritten is said when the part wrote a grant of its state grants (ADR 0547 §5, ADR 0561 §2).
EventGrantWritten = "grant-written"
// EventAsserted is said when the part created a bucket, log or stream a manifest declares (issue 1790).
EventAsserted = "asserted"
)
// Emits is every event the part says.
var Emits = []string{EventAsserted, EventGrantWritten, EventHashesAccepted, EventMembership, EventUserList}
// Store is the part's own state, by local name: the only thing the part writes.
type Store interface {
Get(state, key string) ([]byte, uint64, bool, error)
Put(state, key string, value []byte) (uint64, error)
// Keys and Delete are for the wanted accounts (accounts.go) and the grants (grants.go): every other state is only put.
Keys(state string) ([]string, error)
Delete(state, key string) error
// Update writes a key only while it is still at revision (0: only while it holds nothing), and refuses one changed
// meanwhile with an error wrapping stdio.ErrChanged: the kept bindings are written so, never lost to a second writer
// (issue 1966).
Update(state, key string, value []byte, revision uint64) (uint64, error)
}
// Emitter says one of the part's events.
type Emitter interface {
Emit(event string, body any) error
}
// UserListState is the value of `user-list`: the list the grants part composed (users and grants, no hashes),
// what the bus loaded, and how they differ. Inputs is the digest of all it says but At: a value is written again only
// when it changes.
type UserListState struct {
V int `json:"v"`
Mode string `json:"mode"`
Inputs string `json:"inputs"`
Digest string `json:"digest"`
Users int `json:"users"`
// LeftOut are the assignments the shadow list leaves out, each with why.
LeftOut []Gap `json:"left_out"`
Compare Comparison `json:"compare"`
Loaded *nodeengine.UserListWritten `json:"loaded,omitempty"`
// Credentials says per user whether the vault holds its hash and how its line would differ (ADR 0547 clause 9).
Credentials Credentials `json:"credentials"`
BusNode string `json:"bus_node,omitempty"`
Gaps []Gap `json:"gaps"`
At time.Time `json:"at"`
}
func (s UserListState) inputs() string {
s.At, s.Inputs = time.Time{}, ""
// When the bus node said it loaded a list is no input: the same list said again, at a later time and the key's later
// revision, composes nothing new (issue 1126: written again every 3 s, 2026-10-28).
if s.Loaded != nil {
l := *s.Loaded
l.Revision, l.At = 0, time.Time{}
s.Loaded = &l
}
raw, _ := json.Marshal(s)
sum := sha256.Sum256(raw)
return "sha256:" + hex.EncodeToString(sum[:])
}
// Result is what one composition did.
type Result struct {
UserList UserListState `json:"user_list"`
Revision uint64 `json:"revision"`
Written bool `json:"written"`
Memberships int `json:"memberships"`
MembershipsWritten int `json:"memberships_written"`
Published []string `json:"published,omitempty"`
// KeptLoaded are the users a pass composing part of the mesh gave the union of what it composes and what the loaded list
// and the list in force give them (keeploaded.go, issue 1966), read with grants_user_list's last pass.
KeptLoaded []string `json:"kept_loaded,omitempty"`
NotPublished []string `json:"not_published,omitempty"`
// Retired is, by `<node>.<module>`, the seat verbs its last published membership served that mesh-sdk's seat
// catalogue no longer has (Membership.Retired): the pass keeps it, since the state memberships holds the bare
// membership alone (issue 774).
Retired map[string][]string `json:"retired,omitempty"`
Assertions int `json:"assertions"`
AssertionsWritten int `json:"assertions_written"`
// Asserted is each module whose declared streams this pass created, `<module> at <digest>: created …`, and
// NotAsserted each creation refused, `<module> <stream>: <why>` (issue 1790).
Asserted []string `json:"asserted,omitempty"`
NotAsserted []string `json:"not_asserted,omitempty"`
// Refused is every manifest the indexer holds that this pass could not read, `manifest of <module>: <why>`: its
// module counts as placed nowhere (issue 1945).
Refused []string `json:"refused,omitempty"`
// RootNotFree is each machine the composition needed a root-free word for and did not get one, `<machine>: not
// free, <why>`: a verified sender is composed as from no root-free machine there (issue 1945).
RootNotFree []string `json:"root_not_free,omitempty"`
// Sealed is the list sealed to the bus node as the key `user-list.sealed` names it (ADR 0547 clause 7).
Sealed *SealedState `json:"sealed,omitempty"`
// Consumers is what the pass found and created of the durable consumers an account needs (ADR 0561 §6).
Consumers *ConsumersState `json:"consumers,omitempty"`
// Accounts is what the pass wrote of the wanted accounts the vault mints from (ADR 0561 §2).
Accounts *AccountsWritten `json:"accounts,omitempty"`
// Grants is what the pass wrote of its grants (grants.go).
Grants *GrantsWritten `json:"grants,omitempty"`
// Withheld is what the pass wrote of the hearings it withholds (withheld.go).
Withheld *WithheldWritten `json:"withheld,omitempty"`
// PrincipalsRefused is each principal whose composed grants the composer refused, `<user>: <why>`: it keeps its grants
// of the list in force, or stays out where it holds none (refused.go); the condition mesh-grants.principal-refused says it.
PrincipalsRefused []string `json:"principals_refused,omitempty"`
Errors []string `json:"errors,omitempty"`
PublishedOrAsserted string `json:"published_or_asserted"`
}
// NothingTakesEffect is what every composition says it did on the bus beyond its own state and the memberships.
const NothingTakesEffect = "memberships (in its own state memberships, unless one would leave a seat a placed module claims with no holder, or drop a capability its holder still asks for, which stays a shadow difference), each bucket, log or stream a manifest declares that the bus lacks, created once with the manifest's limits (the operator, 2026-10-26 08:54), the user list the bus node follows, the grants and the account consumers (a module's on another module's stream too); a stream or consumer the bus holds only ever gains the subjects and filters a manifest adds"
// Part is the grants part: it reads, calls and writes through these, and nothing else.
type Part struct {
Read Reader
Call Caller
Store Store
Emit Emitter
// Create creates a declared stream the bus lacks: the process's connection; nil in the tools, which ask the process.
Create Creator
// Logf logs what no state or condition says (an error of acting on an answer): the process's; nil in the tools.
Logf func(string, ...any)
// Objects is the part's object bucket of sealed lists: the process's connection; nil in the tools.
Objects Objects
// Purging takes keys of its own state away whole, values and all (purge.go): the process's connection; nil in the
// tools, where a key taken away is deleted.
Purging Purger
// Consumers creates an account's durable consumer (consumers.go): the process's connection; nil in the tools, which
// compare only.
Consumers ConsumerCreator
// Bus is where the nodes reach the bus, which each wanted account carries (accounts.go): the process's own credential's;
// empty in the tools, which write no account.
Bus BusAddress
// Say says a condition to the operator: the process's; nil in the tools.
Say func(oc.Condition) error
Now func() time.Time
// SeatList is the seat list reader, which gates a module whose seats the list has not named (inputs.go, SeatGate); nil
// gates none.
SeatList SeatGate
cache Manifests
mu sync.Mutex
last Result
made creations
// askSeen is the revision of the last ask to compose (KeyAsked) this part acted on.
askSeen uint64
// heldSaid is the reason of the held-list condition last said (empty: none raised); heldKnown is whether one was said
// since the start, so the first pass after a start always says its state (a condition raised before a restart is cleared
// by it). holdMu guards both.
holdMu sync.Mutex
heldSaid string
heldKnown bool
// differSaid is the account consumers' differences last said as a condition, once differKnown (consumers.go).
differSaid string
differKnown bool
// behindSaid is the account consumers far behind last said as a condition, once behindKnown; behindSeen is each one
// found behind at the pass before, so one look at a burst raises nothing (consumers.go).
behindSaid string
behindKnown bool
behindSeen map[string]bool
// fullCleared is whether the old history-full key was cleared since the start (full.go, ADR 0595 §6).
fullCleared bool
// filterRefused is, by consumer, the refusal of the last addition of filters, so a standing refusal is said once.
filterRefused map[string]string
// refusedSaid is the refused principals last said as a condition, once refusedKnown (refused.go).
refusedSaid string
refusedKnown bool
// heldModulesSaid is the held modules' kept users last said as a condition, once heldModulesKnown (held.go).
heldModulesSaid string
heldModulesKnown bool
// missingSaid is the modules without a grant last said as a condition, once missingKnown (grants.go).
missingSaid string
missingKnown bool
// hashHold are the users whose hash changes held the pass's list, or nil (sealed.go).
hashHold []string
// brokers are the modules whose manifest claims mesh-broker at the last pass (bindings.go), guarded by mu.
brokers map[string]bool
// Views are the part's first views of the owners' states (firstviews.go), and FirstViewContext the process's life
// they ask in: the process's; nil in the tools.
Views *Views
FirstViewContext context.Context
fvSaid firstViewsSaid
}
// keepBrokers keeps the modules whose manifest claims the seat mesh-broker: the only emitters whose created, recreated
// and dropped the part applies (bindings.go).
func (p *Part) keepBrokers(in Inputs) {
brokers := map[string]bool{}
for module, m := range in.Manifests {
if m.ClaimsSeat(meshbroker.Seat) {
brokers[module] = true
}
}
p.mu.Lock()
p.brokers = brokers
p.mu.Unlock()
}
// Brokers are the modules holding mesh-broker as the last pass read them.
func (p *Part) Brokers() map[string]bool {
p.mu.Lock()
defer p.mu.Unlock()
out := map[string]bool{}
for m := range p.brokers {
out[m] = true
}
return out
}
// Ask is the value of KeyAsked: a tool's ask to compose now, and, from grants_assert, to create one module's missing
// streams again even where they were created at this manifest before.
type Ask struct {
By string `json:"by"`
At time.Time `json:"at"`
Assert string `json:"assert,omitempty"`
// Consumer is, from grants_consumer_match, one account consumer `<stream>.<name>` to bring to the composed
// configuration by hand (ADR 0561 §6).
Consumer string `json:"consumer,omitempty"`
// FirstView is, from grants_first_view_again, one first view to ask once more by hand (its state), or AllFirstViews.
FirstView string `json:"first-view,omitempty"`
}
// AllFirstViews asks every first view once more.
const AllFirstViews = "all"
// asked is a new ask, read once per ask; the zero Ask when none.
func (p *Part) asked() Ask {
raw, rev, found, err := p.Store.Get(StateUserList, KeyAsked)
if err != nil || !found || rev == p.askSeen {
return Ask{}
}
p.askSeen = rev
var a Ask
_ = json.Unmarshal(raw, &a)
return a
}
func (p *Part) now() time.Time {
if p.Now == nil {
return time.Now().UTC()
}
return p.Now().UTC()
}
// Compose composes the user list, the memberships and the assertions from what the bus holds now, compares them, and
// writes each of its own keys whose value changed, saying each change. It never stops at a refusal: a read refused is
// a gap, a write refused is said in Errors, and the next event composes again.
func (p *Part) Compose() Result {
// No lock is held across a round trip on the bus: the reads, calls, publishes and writes below take none, and the
// lock is taken only to keep the result, so a tool answers while a pass waits on the bus.
res := Result{PublishedOrAsserted: NothingTakesEffect}
defer func() {
p.mu.Lock()
p.last = res
p.mu.Unlock()
}()
// A first view's condition the condition keeper did not take is said again at every pass until it lands.
p.Views.SayUnsaid()
in := p.inputs()
p.keepBrokers(in)
if err := p.withBound(&in); err != nil {
res.Errors = append(res.Errors, err.Error())
}
for _, g := range in.Refused {
res.Refused = append(res.Refused, g.Input+": "+g.Why)
}
// recs say what runs (memberships, consumers); listRecs what the user list and the grants carry: what runs and what
// the trunk names, so the list grants a build's subjects before the want moves to it (issue 774).
recs, _ := Records(in)
listRecs, left := ListRecords(in)
holdings := HoldingsOf(in)
res.RootNotFree = WithRootFree(&listRecs, p.Call)
recs.RootFree = listRecs.RootFree
st := UserListState{V: 1, Mode: InForce, LeftOut: left, Loaded: in.Loaded, BusNode: in.BusNode,
Gaps: append(append([]Gap(nil), in.Gaps...), MembershipGaps...), At: p.now()}
if st.LeftOut == nil {
st.LeftOut = []Gap{}
}
users, refused, err := ComposeEach(listRecs)
if err != nil {
res.Errors = append(res.Errors, "the shadow list could not be composed: "+err.Error())
}
// A refused principal keeps its grants of the list in force, said once per change (refused.go).
// And a module whose manifest is held (refused, or waiting for a judge that knows a field of it) and whose want names no
// build keeps its users of the list in force (held.go).
loaded, loadErr := LoadedUsers(p.Call)
inForce := p.inForceGrants()
candidates := names(loaded)
for user := range inForce {
candidates = append(candidates, user)
}
heldUsers := heldModuleUsers(in, candidates)
for user, why := range runnerUsersKept(listRecs, inForce) {
heldUsers[user] = why
}
allRefused := mergedRefusals(refused, heldUsers)
kept, _ := p.keptOf(allRefused)
for _, user := range sortedKeys(refused) {
res.PrincipalsRefused = append(res.PrincipalsRefused, user+": "+refused[user])
}
p.sayPrincipalsRefused(refused, kept)
p.sayHeldModules(heldUsers, kept)
users = withKept(users, kept)
// A pass that composes part of the mesh keeps what the bus loaded and the list in force give each user (keeploaded.go).
if why := in.keepsLoaded(); why != "" {
if union := KeepLoaded(users, loaded, inForce); len(union) > 0 {
merged := make(map[string]string, len(allRefused)+len(union))
keptAll := make(map[string]User, len(kept)+len(union))
for u, w := range allRefused {
merged[u] = w
}
for u, k := range kept {
keptAll[u] = k
}
for u, k := range union {
merged[u], keptAll[u] = "keeps what the loaded list gives it: "+why, k
}
users = withKept(slices.DeleteFunc(users, func(u User) bool { _, ok := union[u.User]; return ok }), union)
allRefused, kept = merged, keptAll
res.KeptLoaded = sortedKeys(union)
}
}
users = WithHashes(users, in.Hashes)
st.Digest, st.Users = Digest(users), len(users)
st.Compare = Compare(users, loaded, Placed(in)).Excusing(ExcusedBy(MultiHeld(listRecs)))
if loadErr != nil {
st.Compare.Unread = loadErr.Error()
}
st.Credentials = CredentialsOf(users, loaded, in.Hashes)
st.Inputs = st.inputs()
res.UserList = st
if raw, rev, found, err := p.Store.Get(StateUserList, KeyList); err != nil {
res.Errors = append(res.Errors, fmt.Sprintf("its own %s cannot be read: %v", StateUserList, err))
} else {
var was UserListState
if found && json.Unmarshal(raw, &was) == nil && was.Inputs == st.Inputs {
res.Revision = rev
} else if data, err := json.Marshal(st); err == nil {
if rev, err := p.Store.Put(StateUserList, KeyList, data); err != nil {
res.Errors = append(res.Errors, fmt.Sprintf("its own %s cannot be written: %v", StateUserList, err))
} else {
res.Revision, res.Written = rev, true
p.say(&res, EventUserList, map[string]any{"digest": st.Digest, "revision": rev, "users": st.Users,
"differing": len(st.Compare.Differing()), "mode": InForce})
}
}
}
// The accounts the vault mints from, said in the state only it reads (ADR 0561 §2).
if p.Store != nil {
p.writeAccounts(&res, in, listRecs)
}
// The hearings withheld of what runs, for the self-check's probe D-heard (withheld.go).
if p.Store != nil {
p.writeWithheld(&res, in, recs)
}
list, n, leftOut, dropped, err := ComposeList(listRecs, in.Hashes, allRefused, kept)
// The grants the assigner reads (ADR 0547 §4, §5): one that adds a permission only once the bus loaded a list that
// carries it (issue 774).
if p.Store != nil {
inForce, unknown := p.loadedContents(in, loaded, loadErr)
p.writeGrants(&res, in, listRecs, users, inForce, unknown)
}
// The list itself, sealed to the bus node and named in `user-list.sealed` (ADR 0547 clause 7).
if err != nil {
res.Errors = append(res.Errors, "the user list could not be composed whole: "+err.Error())
} else {
// The users the bus loaded: what a first list must keep where the part has not yet written one.
var bus, lost []string
if st.Compare.Unread == "" {
bus = names(loaded)
lost = holdings.LastHolders(holdings.ByUsers(loaded), holdings.ByUsers(users))
}
p.writeSealed(&res, list, n, leftOut, dropped, in, bus, lost, users, loaded)
}
ms := Memberships(listRecs)
res.Memberships = len(ms)
members := make([]membership.Membership, 0, len(ms))
for _, m := range ms {
members = append(members, m.Membership)
}
seated := holdings.ByMemberships(members...)
// The memberships are written to the state alone and never said on their subject, which nothing reads any more
// (novox/hq ADR 0571 clause 10, issue 774; unconditional since the controller's end, issue 1126).
for _, key := range sortedKeys(ms) {
// The last membership is the state's own (ADR 0571 clause 7), never the assignments stream's, which goes with the
// controller's leftovers (novox/hq ADR 0590 §6).
// The bus module's bindings of the module, as it said them created (bindings.go, ADR 0595 §1, §7).
if binds, err := p.bindingsOf(ms[key].Module); err != nil {
res.Errors = append(res.Errors, err.Error())
} else if binds != nil {
withBinds := ms[key]
withBinds.Membership.Bindings = binds
withBinds.Digest = digestOf(withBinds.Membership)
ms[key] = withBinds
}
was, _, found, err := p.Store.Get(StateMemberships, key)
if err != nil {
res.NotPublished = append(res.NotPublished, key+": not published: the membership last published cannot be read from its own "+StateMemberships+": "+err.Error())
continue
}
// A pass that composes part of the mesh only grows a published membership: it publishes the union of the one last
// published and the one composed (keeploaded.go, issue 1966), so a tool runner or a seat holder goes on serving what it
// serves and still receives what the pass that ends the hold needs.
why := in.keepsLoaded()
if why == "" && in.HadFailed[key] != "" {
why = "the build it runs is unread (" + in.HadFailed[key] + ")"
}
if found && why != "" {
if bare, err := membership.BareGrantsMembership(was); err == nil {
if body, err := json.Marshal(ms[key].Membership); err == nil {
if grown, kept, err := UnionMembership(bare, body); err == nil && len(kept) > 0 {
var u membership.Membership
if json.Unmarshal(grown, &u) == nil {
withUnion := ms[key]
withUnion.Membership = u
withUnion.Digest = digestOf(u)
ms[key] = withUnion
res.NotPublished = append(res.NotPublished, key+": kept beyond this pass: "+why+", this pass composes it without "+strings.Join(kept, ", "))
}
}
}
}
}
m, now := publish(was, found, ms[key], holdings, seated)
// Published is written to the state, where every runtime reads it (process.FollowMembership); only one
// published, so a membership held back never reaches a reader.
if now {
body := membership.EncodeGrantsMembership(m.Membership)
if _, err := p.Store.Put(StateMemberships, key, body); err != nil {
res.Errors = append(res.Errors, fmt.Sprintf("its own %s %s cannot be written: %v", StateMemberships, key, err))
m.Published, now = "not published (tried again at the next event): its own "+StateMemberships+" cannot be written", false
} else {
res.MembershipsWritten++
}
}
// One unchanged is written bare once where the state still holds the wrapper written before 2026-10-30.
if m.Published == MembershipUnchanged && p.changed(&res, StateMemberships, key, membership.EncodeGrantsMembership(m.Membership)) {
res.MembershipsWritten++
}
switch {
case now:
res.Published = append(res.Published, key)
p.say(&res, EventMembership, map[string]any{"node": m.Node, "module": m.Module, "digest": m.Digest, "published": true})
case m.Published != MembershipUnchanged:
res.NotPublished = append(res.NotPublished, key+": "+m.Published)
}
if len(m.Retired) > 0 {
if res.Retired == nil {
res.Retired = map[string][]string{}
}
res.Retired[key] = m.Retired
}
}
p.clearFull()
held, heldErr := HeldStreams(p.Call)
var ask Ask
if p.Create != nil || p.Consumers != nil {
ask = p.asked()
}
forced := ask.Assert
for _, module := range sortedKeys(in.Manifests) {
if !assigned(in, module) {
continue
}
a := Assert(module, in.Digests[module], in.Manifests[module], in.Nodes, held, heldErr)
// Created only where the bus was read and lacks it, never changed where it holds it (issue 1790).
if p.Create != nil && heldErr == nil {
if made, raises := createMissing(p.Create, &p.made, &a, in.Manifests[module], in.Nodes, forced == module); len(made) > 0 {
res.Asserted = append(res.Asserted, fmt.Sprintf("%s at %s: created %s", module, a.Manifest, strings.Join(made, ", ")))
if p.Logf != nil {
p.Logf("%s at %s: asserted on the bus: %s", module, a.Manifest, strings.Join(made, ", "))
}
p.say(&res, EventAsserted, map[string]any{"module": module, "manifest": a.Manifest, "created": made, "raised": raises})
}
for _, why := range notCreated(a) {
res.NotAsserted = append(res.NotAsserted, module+" "+why)
}
}
res.Assertions++
data, _ := json.Marshal(a)
if p.changed(&res, StateAssertions, module, data) {
res.AssertionsWritten++
}
}
p.consumers(&res, recs, ask.Consumer)
p.sayFirstViews(&res)
if ask.FirstView != "" && p.Views != nil && p.FirstViewContext != nil {
state := ask.FirstView
if state == AllFirstViews {
state = ""
}
if asked, err := p.Views.Again(p.FirstViewContext, state, "by hand: "+ask.By); err != nil {
res.Errors = append(res.Errors, err.Error())
} else if p.Logf != nil {
p.Logf("asked the first views %v once more by hand, for %s", asked, ask.By)
}
}
return res
}
// Last is the result of the last pass this part made, kept under the lock alone.
func (p *Part) Last() Result {
p.mu.Lock()
defer p.mu.Unlock()
return p.last
}
func assigned(in Inputs, module string) bool {
// The node-engine runs on every node though no node is assigned it (novox/hq ADR 0563 §2, §3): its manifest is
// placed everywhere, so its declared state is asserted and created as a placed module's.
if module == buscompose.EngineModule {
return true
}
for _, ms := range in.Assigned {
for _, m := range ms {
if m == module {
return true
}
}
}
return false
}
// changed writes key when its value differs from the one held, and says whether it wrote.
func (p *Part) changed(res *Result, state, key string, data []byte) bool {
raw, _, found, err := p.Store.Get(state, key)
if err != nil {
res.Errors = append(res.Errors, fmt.Sprintf("its own %s %s cannot be read: %v", state, key, err))
return false
}
if found && bytes.Equal(raw, data) {
return false
}
if _, err := p.Store.Put(state, key, data); err != nil {
res.Errors = append(res.Errors, fmt.Sprintf("its own %s %s cannot be written: %v", state, key, err))
return false
}
return true
}
func (p *Part) say(res *Result, event string, body any) {
if p.Emit == nil {
return
}
if err := p.Emit.Emit(event, body); err != nil {
res.Errors = append(res.Errors, fmt.Sprintf("the event %s could not be said: %v", event, err))
}
}
func sortedKeys[V any](m map[string]V) []string {
out := make([]string, 0, len(m))
for k := range m {
out = append(out, k)
}
sort.Strings(out)
return out
}
func names(us []User) []string {
out := make([]string, 0, len(us))
for _, u := range us {
out = append(out, u.User)
}
return out
}
// inputs reads what the part composes from, a module whose seats the seat list has not named held back (SeatGate).
func (p *Part) inputs() Inputs {
p.cache.mu.Lock()
p.cache.gate = p.SeatList
p.cache.mu.Unlock()
return ReadInputs(p.Read, &p.cache)
}