Files
mesh-sdk/go/process/gate.go
T
jschoubben d3980ded07 A3f item 1: a process holder's marked seat verb is held through go/approval
conn.AnswerSeat holds a verb its seat definition marks needs-approval through the
holder's gate (process.Conn.Gate, go/approval held.go): waiting calls call.<id>,
act.<escaped digest> by create or compare-and-set, claimed.<ask id> by create; a
call carrying the x-wait: held header answered {asked, call} in the reply's field
held; on the router's record read under the module's name, the oldest waiting call
run once within 30 minutes of the Approve and recorded through
mesh-hand-acts.warranted, the others asked anew; declined unasked for 10 minutes;
expired and refused ends; every end said on gated-ended.<caller>.<call>; the read
at start; gate-unanswered past the ask's 60 minutes; the verb approvals. A call
without the header takes ADR 0286 §37's path. process.Conn's seat call sends the
header on every seat call and, answered held, waits on its own gated-ended.

Builds: design 47 G §677, §700, lead answers A-158, A-186, A-209, A-219, A-224, A-228, A-244

Claude-Session: https://claude.ai/code/session_01NcbgfGKaxw984XyAPfLm4h
2026-11-12 09:28:06 +01:00

130 lines
5.1 KiB
Go

package process
import (
"context"
"encoding/json"
"errors"
"fmt"
"strings"
"time"
"github.com/nats-io/nats.go/jetstream"
stdio "git.novox.be/novox/mesh-sdk/go"
"git.novox.be/novox/mesh-sdk/go/approval"
"git.novox.be/novox/mesh-sdk/go/busretry"
"git.novox.be/novox/mesh-sdk/go/seatdef"
)
// Gating is what a holder's gate reaches beyond this connection (design 47 G §677; lead answers A-219, A-224, A-228): the
// operator channel (mesh-contracts' operatorchannelclient.GateChannel over this connection, reading the router's record
// under the module's own name, §700) and the holder's held finding writer.
type Gating struct {
Channel approval.Channel
Findings approval.FindingWriter
// Build is the holder's own fingerprint; empty reads it from the process's environment (OwnBuild).
Build string
Logf func(string, ...any)
}
// Gate is the holder's gate over this connection (§677): its state `approvals`, its calls of the seat `mesh-hand-acts`'s
// `warranted` to record a run, and its seat's `gated-ended`, each on the process's own account. AnswerSeat holds the
// seat's marked verbs through it (Gated); the process hears its decided with HearDecided and reads it at its start with
// the gate's StartHolding.
func (c *Conn) Gate(g Gating) *approval.Gate {
build := g.Build
if build == "" {
build = OwnBuild().Fingerprint
}
return approval.New(approval.Deps{Node: c.account.Node, Runner: c.account.Module, Holding: true, Build: build,
State: c.State(approval.StateName), Channel: g.Channel, Findings: g.Findings, Logf: g.Logf,
Warranted: func(asker, ask string, open func() (bool, error)) error {
_, err := c.CallKeptWhileOpen(busretry.Open{Key: ask, Open: func(string) (bool, error) { return open() }, Logf: g.Logf},
"seat:mesh-hand-acts.warranted", map[string]string{"asker": asker, "ask": ask})
return err
},
Say: func(subject string, body any, id string) error {
return c.emitOn(subject, body, []stdio.EmitOptions{{MessageID: id}})
}})
}
// HearDecided hands the gate each `decided` of the holder's own asks that hub hears (§677): only the ask's id is read
// from it, the gate reading the router's own record.
func HearDecided(g *approval.Gate, ch approval.Channel, module string, hub *EventHub) error {
return hub.Handle(ch.Decided(module), func(m jetstream.Msg) {
var w struct {
Ask string `json:"ask"`
}
if err := json.Unmarshal(m.Data(), &w); err != nil || w.Ask == "" {
_ = m.Term()
return
}
g.Decided(w.Ask)
_ = m.Ack()
})
}
// HeldWithin bounds a caller's wait on a held call's end: the ask's 60 minutes and the 30 a run may come after its choice.
var HeldWithin = approval.AskLasts + approval.RunWithin
// awaitGated waits on the held call's own end on its seat's gated-ended, from the last said on its subject (so an end said
// before the wait began is read), and is its answer: `{answer, approval}` when it ran (lead answer A-158), its words as
// the call's error otherwise. Every failure is an answer, never asked again: the call reached its holder (ADR 0668 §2).
func (c *Conn) awaitGated(subject string, held approval.Held) (json.RawMessage, error) {
seat, ok := seatOfSubject(subject)
if !ok {
return nil, answered(fmt.Sprintf("%s answered held, and is no seat's subject", subject))
}
js, err := jetstream.New(c.nc)
if err != nil {
return nil, answered(err.Error())
}
on := approval.GatedEndedSubject(seat, c.account.User, held.Call)
stream := seatdef.KeptStream(seat, seatdef.Message{Name: approval.GatedEndedEvent})
ctx, cancel := context.WithTimeout(context.Background(), HeldWithin)
defer cancel()
cons, err := js.OrderedConsumer(ctx, stream, jetstream.OrderedConsumerConfig{FilterSubjects: []string{on},
DeliverPolicy: jetstream.DeliverLastPerSubjectPolicy})
if err != nil {
return nil, answered(fmt.Sprintf("the call %s waits on ask %s, and its end on %s cannot be heard: %v; %s.approvals reads it",
held.Call, held.Asked, stream, err, seat))
}
for {
m, err := cons.Next(jetstream.FetchMaxWait(time.Minute))
if err == nil {
_ = m.Ack()
return endOf(m.Data())
}
if ctx.Err() != nil || !errors.Is(err, jetstream.ErrNoMessages) && !errors.Is(err, context.DeadlineExceeded) {
return nil, answered(fmt.Sprintf("the call %s waits on ask %s, and no end of it was heard (%v); %s.approvals reads it",
held.Call, held.Asked, err, seat))
}
}
}
// endOf is a gated-ended's body as the call's answer.
func endOf(data []byte) (json.RawMessage, error) {
var e approval.GatedEnded
if err := json.Unmarshal(data, &e); err != nil {
return nil, answered("the end of the call is not read: " + err.Error())
}
if e.Outcome != approval.OutcomeRan || e.Error != "" {
words := e.Error
if words == "" {
words = "the call ended " + e.Outcome
}
return nil, answered(fmt.Sprintf("Not run: %s (ask %s, call %s)", words, e.Ask, e.Call))
}
return json.Marshal(approval.Answered{Answer: e.Answer, Approval: e.Approval})
}
// seatOfSubject is the seat a call subject `mesh.seat.<seat>.…` names.
func seatOfSubject(subject string) (string, bool) {
rest, ok := strings.CutPrefix(subject, "mesh.seat.")
if !ok {
return "", false
}
seat, _, ok := strings.Cut(rest, ".")
return seat, ok && seat != ""
}