Files
mesh-tools/node-tools/internal/bus/chunked.go
T

96 lines
3.2 KiB
Go

package bus
import (
"bytes"
"crypto/rand"
"encoding/base64"
"encoding/hex"
"encoding/json"
"errors"
"fmt"
"strings"
"time"
"github.com/nats-io/nats.go"
"git.novox.be/novox/mesh-sdk/go/buscalls"
)
// FileField is the member of a chunked verb's answer that carries the file received: its whole bytes, base64 (design 47 G
// A-249).
const FileField = "file"
// AskAsChunked is AskAs of a seat's verb whose definition says `chunks` (design 47 G §681, A-237, A-249): it chooses the call
// token (the body's `call`), hears the chunk subjects of that call before it asks, receives the chunks the verb's answer
// counts with buscalls.SeatChunks, each acknowledged before the next, and answers the verb's own answer with the whole
// file added as FileField. The asking bundle receives no chunk itself.
func (c *Conn) AskAsChunked(module, key string, body any) (Answered, error) {
seat, verb, ok := strings.Cut(strings.TrimPrefix(key, "seat:"), ".")
if !strings.HasPrefix(key, "seat:") || !ok || strings.Contains(verb, "@") {
return Answered{}, fmt.Errorf("%q is no seat:<seat>.<verb> of no one machine, which a chunked answer needs", key)
}
args := map[string]any{}
if raw, err := json.Marshal(body); err != nil {
return Answered{}, err
} else if string(raw) != "null" {
if err := json.Unmarshal(raw, &args); err != nil {
return Answered{}, fmt.Errorf("the arguments of %s are not an object: %w", key, err)
}
}
var bits [16]byte
if _, err := rand.Read(bits[:]); err != nil {
return Answered{}, err
}
send := buscalls.SeatChunks{Seat: seat, Verb: verb, Call: hex.EncodeToString(bits[:])}
args["call"] = send.Call
chunks := make(chan *nats.Msg, 4)
sub, err := c.nc.ChanSubscribe(send.Chunks(), chunks)
if err != nil {
return Answered{}, fmt.Errorf("the chunks of %s cannot be heard: %w", key, err)
}
defer func() { _ = sub.Unsubscribe() }()
answered, err := c.AskAs(module, key, args)
if err != nil {
return answered, err
}
fields := map[string]json.RawMessage{}
if err := json.Unmarshal(answered.Result, &fields); err != nil {
return answered, fmt.Errorf("%s answered what is no object: %w", key, err)
}
var count int64
if raw, has := fields["chunks"]; has {
if err := json.Unmarshal(raw, &count); err != nil {
return answered, fmt.Errorf("%s answered chunks that is no count: %w", key, err)
}
}
var file bytes.Buffer
if count > 0 {
next := func() (string, []byte, error) {
select {
case m := <-chunks:
return m.Subject, m.Data, nil
case <-time.After(buscalls.SeatAckWithin):
return "", nil, buscalls.ErrSeatChunkMissed
}
}
ack := func(subject string) error {
_, err := c.nc.Request(subject, []byte("{}"), buscalls.SeatAckWithin)
return err
}
if _, err := send.Receive(count, next, ack, &file); err != nil {
return answered, fmt.Errorf("the chunks of %s: %w", key, err)
}
}
if _, taken := fields[FileField]; taken {
return answered, errors.New(key + " answers a member named " + FileField + " itself")
}
enc, _ := json.Marshal(base64.StdEncoding.EncodeToString(file.Bytes()))
fields[FileField] = enc
out, err := json.Marshal(fields)
if err != nil {
return answered, err
}
answered.Result = out
return answered, nil
}