Main reverted G's half merge (#189, reverted by8afc3ebin #191) and then took the reader release's S1-S5, which copied G's code. G's form holds where they differ (process_bus.go, seats.go, state.go and their tests); the release's kept names stay so main's callers keep building: SeatViewPrefix beside SeatReadPrefix, and QueueInvoke's empty strings on not-ok (with its test TestQueueInvoke). The tree differs from G's headf30dee54only by those and the release's two tests (objects cap option, seat states and stream kinds). Builds: design 47 G (the milestone's sdk part, as atf30dee54) Claude-Session: https://claude.ai/code/session_01NcbgfGKaxw984XyAPfLm4h
111 lines
3.6 KiB
Go
111 lines
3.6 KiB
Go
package process
|
|
|
|
import (
|
|
"bytes"
|
|
"context"
|
|
"encoding/base64"
|
|
"encoding/json"
|
|
"fmt"
|
|
"io"
|
|
"time"
|
|
|
|
"github.com/nats-io/nats.go"
|
|
|
|
"git.novox.be/novox/mesh-sdk/go/buscalls"
|
|
"git.novox.be/novox/mesh-sdk/go/calladdress"
|
|
)
|
|
|
|
// CallChunks calls a seat verb whose answer file is sent in chunks (a verb its seat definition marks `chunks`, design 47 G
|
|
// §681, A-139), the one way a process receives such an answer (A-291): it subscribes to the chunks of call (the 128 random
|
|
// bits the verb's input carries as its call) before it asks, so none is missed, asks key ("seat:<seat>.<verb>[@<node>]")
|
|
// with body as Call does, hands the verb's answer to accept (nil accepts every answer; an error ends the call before a chunk
|
|
// is taken), then receives the number of chunks the answer's member `chunks` names, in order, acknowledging each, into w.
|
|
// It answers the verb's answer. A chunk that does not come within buscalls.SeatAckWithin, or ctx ending, ends the receive.
|
|
func (c *Conn) CallChunks(ctx context.Context, key, call string, body any, accept func(answer json.RawMessage) error, w io.Writer) (json.RawMessage, error) {
|
|
a, err := calladdress.Parse(key)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
if a.Seat == "" {
|
|
return nil, fmt.Errorf("%q names no seat verb, so it sends no chunks", key)
|
|
}
|
|
if call == "" {
|
|
return nil, fmt.Errorf("the call of %s names no call token for its chunks", key)
|
|
}
|
|
send := buscalls.SeatChunks{Seat: a.Seat, Verb: a.Verb, Call: call}
|
|
chunks := make(chan *nats.Msg, 4)
|
|
sub, err := c.nc.ChanSubscribe(send.Chunks(), chunks)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("the chunks of %s cannot be heard: %w", key, err)
|
|
}
|
|
defer func() { _ = sub.Unsubscribe() }()
|
|
answer, err := c.Call(key, body)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
if accept != nil {
|
|
if err := accept(answer); err != nil {
|
|
return answer, err
|
|
}
|
|
}
|
|
var count struct {
|
|
Chunks int64 `json:"chunks"`
|
|
}
|
|
if err := json.Unmarshal(answer, &count); err != nil {
|
|
return answer, fmt.Errorf("%s answered no count of its chunks: %w", key, err)
|
|
}
|
|
next := func() (string, []byte, error) {
|
|
bound := time.NewTimer(buscalls.SeatAckWithin)
|
|
defer bound.Stop()
|
|
select {
|
|
case m := <-chunks:
|
|
return m.Subject, m.Data, nil
|
|
case <-bound.C:
|
|
return "", nil, buscalls.ErrSeatChunkMissed
|
|
case <-ctx.Done():
|
|
return "", nil, ctx.Err()
|
|
}
|
|
}
|
|
ack := func(subject string) error {
|
|
_, err := c.nc.Request(subject, []byte("{}"), buscalls.SeatAckWithin)
|
|
return err
|
|
}
|
|
if _, err := send.Receive(count.Chunks, next, ack, w); err != nil {
|
|
return answer, fmt.Errorf("the answer of %s: %w", key, err)
|
|
}
|
|
return answer, nil
|
|
}
|
|
|
|
// CallFile is CallChunks for an answer read whole, in the form a bundle's ordinary seat call answers a chunked verb (design 47
|
|
// G A-249, A-253): the verb's answer with the whole file added as its member `file` (base64), none when no chunk was sent. The
|
|
// call token is body's member `call`.
|
|
func (c *Conn) CallFile(key string, body any) (json.RawMessage, error) {
|
|
call := callOf(body)
|
|
var file bytes.Buffer
|
|
answer, err := c.CallChunks(context.Background(), key, call, body, nil, &file)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
var whole map[string]any
|
|
if err := json.Unmarshal(answer, &whole); err != nil {
|
|
return nil, fmt.Errorf("%s answered what is no object: %w", key, err)
|
|
}
|
|
if file.Len() > 0 {
|
|
whole["file"] = base64.StdEncoding.EncodeToString(file.Bytes())
|
|
}
|
|
return json.Marshal(whole)
|
|
}
|
|
|
|
// callOf is the member call of a verb's input.
|
|
func callOf(body any) string {
|
|
raw, err := json.Marshal(body)
|
|
if err != nil {
|
|
return ""
|
|
}
|
|
var in struct {
|
|
Call string `json:"call"`
|
|
}
|
|
_ = json.Unmarshal(raw, &in)
|
|
return in.Call
|
|
}
|